aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.checkpatch.conf2
-rw-r--r--.github/FUNDING.yml1
-rw-r--r--README.md41
-rwxr-xr-xcontrib/csv-encrypt-columns.py79
-rwxr-xr-xcontrib/eidtool.py73
-rwxr-xr-xcontrib/es2p_client.py79
-rwxr-xr-xcontrib/jenkins.sh20
-rwxr-xr-xcontrib/sim-rest-client.py1
-rwxr-xr-xcontrib/sim-rest-server.py25
-rwxr-xr-xcontrib/unber.py39
-rw-r--r--docs/card-key-provider.rst126
-rw-r--r--docs/index.rst1
-rw-r--r--docs/osmo-smdpp.rst114
-rw-r--r--docs/shell.rst881
-rw-r--r--docs/suci-tutorial.rst42
-rwxr-xr-xosmo-smdpp.py567
-rwxr-xr-xota_test.py50
-rwxr-xr-xpySim-prog.py150
-rwxr-xr-xpySim-read.py19
-rwxr-xr-xpySim-shell.py358
-rwxr-xr-xpySim-trace.py65
-rw-r--r--pySim/apdu/__init__.py60
-rw-r--r--pySim/apdu/ts_102_221.py53
-rw-r--r--pySim/apdu/ts_31_102.py6
-rw-r--r--pySim/apdu/ts_51_011.py2
-rw-r--r--pySim/apdu_source/__init__.py3
-rw-r--r--pySim/apdu_source/gsmtap.py14
-rw-r--r--pySim/apdu_source/pyshark_gsmtap.py16
-rw-r--r--pySim/apdu_source/pyshark_rspro.py11
-rw-r--r--pySim/apdu_source/tca_loader_log.py48
-rw-r--r--pySim/app.py122
-rw-r--r--pySim/ara_m.py72
-rw-r--r--pySim/card_handler.py5
-rw-r--r--pySim/card_key_provider.py61
-rw-r--r--pySim/cards.py1764
-rw-r--r--pySim/cat.py215
-rw-r--r--pySim/cdma_ruim.py29
-rw-r--r--pySim/commands.py482
-rw-r--r--pySim/construct.py335
-rw-r--r--pySim/esim/__init__.py93
-rw-r--r--pySim/esim/asn1/rsp/PKIX1Explicit88.asn657
-rw-r--r--pySim/esim/asn1/rsp/PKIX1Implicit88.asn343
-rw-r--r--pySim/esim/asn1/rsp/rsp.asn785
-rw-r--r--pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn1126
-rw-r--r--pySim/esim/bsp.py296
-rw-r--r--pySim/esim/es2p.py462
-rw-r--r--pySim/esim/es8p.py185
-rw-r--r--pySim/esim/rsp.py131
-rw-r--r--pySim/esim/saip/__init__.py513
-rw-r--r--pySim/esim/saip/oid.py77
-rw-r--r--pySim/esim/saip/personalization.py324
-rw-r--r--pySim/esim/saip/templates.py675
-rw-r--r--pySim/esim/saip/validation.py134
-rw-r--r--pySim/esim/x509_cert.py210
-rw-r--r--pySim/euicc.py549
-rw-r--r--pySim/exceptions.py3
-rw-r--r--pySim/filesystem.py700
-rw-r--r--pySim/global_platform.py256
-rw-r--r--pySim/global_platform/__init__.py877
-rw-r--r--pySim/global_platform/scp.py534
-rw-r--r--pySim/global_platform/uicc.py107
-rw-r--r--pySim/gsm_r.py80
-rw-r--r--pySim/gsmtap.py4
-rw-r--r--pySim/iso7816_4.py2
-rw-r--r--pySim/jsonpath.py7
-rw-r--r--pySim/legacy/__init__.py0
-rw-r--r--pySim/legacy/cards.py1664
-rw-r--r--pySim/legacy/ts_31_102.py134
-rw-r--r--pySim/legacy/ts_31_103.py43
-rw-r--r--pySim/legacy/ts_51_011.py257
-rw-r--r--pySim/legacy/utils.py332
-rw-r--r--pySim/ota.py50
-rw-r--r--pySim/profile.py38
-rw-r--r--pySim/runtime.py550
-rw-r--r--pySim/secure_channel.py37
-rw-r--r--pySim/sms.py57
-rw-r--r--pySim/sysmocom_sja2.py157
-rw-r--r--pySim/tlv.py162
-rw-r--r--pySim/transport/__init__.py158
-rw-r--r--pySim/transport/calypso.py39
-rw-r--r--pySim/transport/modem_atcmd.py54
-rw-r--r--pySim/transport/pcsc.py74
-rw-r--r--pySim/transport/serial.py55
-rw-r--r--pySim/ts_102_221.py212
-rw-r--r--pySim/ts_102_222.py59
-rw-r--r--pySim/ts_102_310.py114
-rw-r--r--pySim/ts_31_102.py586
-rw-r--r--pySim/ts_31_102_telecom.py32
-rw-r--r--pySim/ts_31_103.py91
-rw-r--r--pySim/ts_31_104.py4
-rw-r--r--pySim/ts_51_011.py368
-rw-r--r--pySim/utils.py612
-rw-r--r--pysim-testdata/Fairwaves-SIM.ok2
-rw-r--r--pysim-testdata/Wavemobile-SIM.ok4
-rw-r--r--pysim-testdata/fakemagicsim.ok1
-rw-r--r--pysim-testdata/pySim-trace_test_gsmtap.pcapngbin0 -> 116540 bytes
-rw-r--r--pysim-testdata/pySim-trace_test_gsmtap.pcapng.ok618
-rw-r--r--pysim-testdata/sysmoISIM-SJA2.ok6
-rw-r--r--pysim-testdata/sysmoUSIM-SJS1.ok6
-rw-r--r--pysim-testdata/sysmosim-gr1.ok1
-rw-r--r--requirements.txt3
-rw-r--r--setup.py13
-rw-r--r--smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_BRP.derbin0 -> 597 bytes
-rw-r--r--smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_BRP.pem15
-rw-r--r--smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_NIST.derbin0 -> 596 bytes
-rw-r--r--smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_NIST.pem15
-rw-r--r--smdpp-data/certs/CertificateIssuer/CI-csr.cnf25
-rw-r--r--smdpp-data/certs/DPauth/CERT_S_SM_DP2auth_ECDSA_BRP.derbin0 -> 575 bytes
-rw-r--r--smdpp-data/certs/DPauth/CERT_S_SM_DP2auth_ECDSA_NIST.derbin0 -> 573 bytes
-rw-r--r--smdpp-data/certs/DPauth/CERT_S_SM_DPauth_ECDSA_BRP.derbin0 -> 573 bytes
-rw-r--r--smdpp-data/certs/DPauth/CERT_S_SM_DPauth_ECDSA_NIST.derbin0 -> 572 bytes
-rw-r--r--smdpp-data/certs/DPauth/PK_S_SM_DP2auth_ECDSA_BRP.pem4
-rw-r--r--smdpp-data/certs/DPauth/PK_S_SM_DP2auth_ECDSA_NIST.pem4
-rw-r--r--smdpp-data/certs/DPauth/PK_S_SM_DPauth_ECDSA_BRP.pem4
-rw-r--r--smdpp-data/certs/DPauth/PK_S_SM_DPauth_ECDSA_NIST.pem4
-rw-r--r--smdpp-data/certs/DPauth/SK_S_SM_DP2auth_ECDSA_BRP.pem8
-rw-r--r--smdpp-data/certs/DPauth/SK_S_SM_DP2auth_ECDSA_NIST.pem8
-rw-r--r--smdpp-data/certs/DPauth/SK_S_SM_DPauth_ECDSA_BRP.pem8
-rw-r--r--smdpp-data/certs/DPauth/SK_S_SM_DPauth_ECDSA_NIST.pem8
-rw-r--r--smdpp-data/certs/DPauth/data_sig.der1
-rw-r--r--smdpp-data/certs/DPpb/CERT_S_SM_DP2pb_ECDSA_BRP.derbin0 -> 574 bytes
-rw-r--r--smdpp-data/certs/DPpb/CERT_S_SM_DP2pb_ECDSA_NIST.derbin0 -> 575 bytes
-rw-r--r--smdpp-data/certs/DPpb/CERT_S_SM_DPpb_ECDSA_BRP.derbin0 -> 573 bytes
-rw-r--r--smdpp-data/certs/DPpb/CERT_S_SM_DPpb_ECDSA_NIST.derbin0 -> 573 bytes
-rw-r--r--smdpp-data/certs/DPpb/PK_S_SM_DP2pb_ECDSA_BRP.pem4
-rw-r--r--smdpp-data/certs/DPpb/PK_S_SM_DP2pb_ECDSA_NIST.pem4
-rw-r--r--smdpp-data/certs/DPpb/PK_S_SM_DPpb_ECDSA_BRP.pem4
-rw-r--r--smdpp-data/certs/DPpb/PK_S_SM_DPpb_ECDSA_NIST.pem4
-rw-r--r--smdpp-data/certs/DPpb/SK_S_SM_DP2pb_ECDSA_BRP.pem8
-rw-r--r--smdpp-data/certs/DPpb/SK_S_SM_DP2pb_ECDSA_NIST.pem8
-rw-r--r--smdpp-data/certs/DPpb/SK_S_SM_DPpb_ECDSA_BRP.pem8
-rw-r--r--smdpp-data/certs/DPpb/SK_S_SM_DPpb_ECDSA_NIST.pem8
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.csr.cnf10
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.ext.cnf14
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.csr.cnf10
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.ext.cnf14
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.csr.cnf10
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.ext.cnf14
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS.csr.cnf10
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS.ext.cnf14
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_BRP.derbin0 -> 646 bytes
-rw-r--r--smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP2_TLS.derbin0 -> 646 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP4_TLS.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP8_TLS.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_BRP.derbin0 -> 646 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_NIST.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP2_TLS.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP4_TLS.derbin0 -> 646 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP8_TLS.derbin0 -> 646 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_BRP.derbin0 -> 646 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_NIST.derbin0 -> 645 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP2_TLS.derbin0 -> 646 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP4_TLS.derbin0 -> 647 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP8_TLS.derbin0 -> 648 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_BRP.derbin0 -> 648 bytes
-rw-r--r--smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_NIST.derbin0 -> 646 bytes
-rw-r--r--smdpp-data/certs/DPtls/PK_S_SM_DP2_TLS_NIST.pem4
-rw-r--r--smdpp-data/certs/DPtls/PK_S_SM_DP4_TLS.pem4
-rw-r--r--smdpp-data/certs/DPtls/PK_S_SM_DP8_TLS.pem4
-rw-r--r--smdpp-data/certs/DPtls/PK_S_SM_DP_TLS_BRP.pem4
-rw-r--r--smdpp-data/certs/DPtls/PK_S_SM_DP_TLS_NIST.pem4
-rw-r--r--smdpp-data/certs/DPtls/SK_S_SM_DP2_TLS_NIST.pem8
-rw-r--r--smdpp-data/certs/DPtls/SK_S_SM_DP4_TLS.pem8
-rw-r--r--smdpp-data/certs/DPtls/SK_S_SM_DP8_TLS.pem8
-rw-r--r--smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_BRP.pem8
-rw-r--r--smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_NIST.pem8
-rw-r--r--smdpp-data/certs/README1
-rw-r--r--smdpp-data/upp/TS48V1-A-UNIQUE.derbin0 -> 11768 bytes
-rw-r--r--smdpp-data/upp/TS48V1-B-UNIQUE.derbin0 -> 11785 bytes
-rw-r--r--smdpp-data/upp/TS48V2-SAIP2-1-BERTLV-UNIQUE.derbin0 -> 12257 bytes
-rw-r--r--smdpp-data/upp/TS48V2-SAIP2-1-NOBERTLV-UNIQUE.derbin0 -> 12207 bytes
-rw-r--r--smdpp-data/upp/TS48V2-SAIP2-3-BERTLV-UNIQUE.derbin0 -> 12309 bytes
-rw-r--r--smdpp-data/upp/TS48V2-SAIP2-3-NOBERTLV-UNIQUE.derbin0 -> 12251 bytes
-rw-r--r--smdpp-data/upp/TS48V3-SAIP2-1-BERTLV-UNIQUE.derbin0 -> 12117 bytes
-rw-r--r--smdpp-data/upp/TS48V3-SAIP2-1-NOBERTLV-UNIQUE.derbin0 -> 12065 bytes
-rw-r--r--smdpp-data/upp/TS48V3-SAIP2-3-BERTLV-UNIQUE.derbin0 -> 12166 bytes
-rw-r--r--smdpp-data/upp/TS48V3-SAIP2-3-NOBERTLV-UNIQUE.derbin0 -> 12105 bytes
-rw-r--r--smdpp-data/upp/TS48V4-SAIP2-1A-NOBERTLV-UNIQUE.derbin0 -> 12079 bytes
-rw-r--r--smdpp-data/upp/TS48V4-SAIP2-1B-NOBERTLV-UNIQUE.derbin0 -> 12096 bytes
-rw-r--r--smdpp-data/upp/TS48V4-SAIP2-3-BERTLV-UNIQUE.derbin0 -> 12180 bytes
-rw-r--r--smdpp-data/upp/TS48V4-SAIP2-3-NOBERTLV-UNIQUE.derbin0 -> 12119 bytes
-rw-r--r--smdpp-data/upp/TS48V5-SAIP2-1A-NOBERTLV-UNIQUE.derbin0 -> 12060 bytes
-rw-r--r--smdpp-data/upp/TS48V5-SAIP2-1B-NOBERTLV-UNIQUE.derbin0 -> 12140 bytes
-rw-r--r--smdpp-data/upp/TS48V5-SAIP2-3-BERTLV-SUCI-UNIQUE.derbin0 -> 12284 bytes
-rw-r--r--smdpp-data/upp/TS48V5-SAIP2-3-NOBERTLV-UNIQUE.derbin0 -> 12163 bytes
-rw-r--r--smdpp-data/upp/TS48v1_A.derbin0 -> 11768 bytes
-rw-r--r--smdpp-data/upp/TS48v1_B.derbin0 -> 11785 bytes
-rw-r--r--smdpp-data/upp/TS48v2_SAIP2.1_BERTLV.derbin0 -> 12257 bytes
-rw-r--r--smdpp-data/upp/TS48v2_SAIP2.1_NoBERTLV.derbin0 -> 12207 bytes
-rw-r--r--smdpp-data/upp/TS48v2_SAIP2.3_BERTLV.derbin0 -> 12309 bytes
-rw-r--r--smdpp-data/upp/TS48v2_SAIP2.3_NoBERTLV.derbin0 -> 12251 bytes
-rw-r--r--smdpp-data/upp/TS48v3_SAIP2.1_BERTLV.derbin0 -> 12117 bytes
-rw-r--r--smdpp-data/upp/TS48v3_SAIP2.1_NoBERTLV.derbin0 -> 12065 bytes
-rw-r--r--smdpp-data/upp/TS48v3_SAIP2.3_BERTLV.derbin0 -> 12166 bytes
-rw-r--r--smdpp-data/upp/TS48v3_SAIP2.3_NoBERTLV.derbin0 -> 12105 bytes
-rw-r--r--smdpp-data/upp/TS48v4_SAIP2.1A_NoBERTLV.derbin0 -> 12079 bytes
-rw-r--r--smdpp-data/upp/TS48v4_SAIP2.1B_NoBERTLV.derbin0 -> 12096 bytes
-rw-r--r--smdpp-data/upp/TS48v4_SAIP2.3_BERTLV.derbin0 -> 12180 bytes
-rw-r--r--smdpp-data/upp/TS48v4_SAIP2.3_NoBERTLV.derbin0 -> 12119 bytes
-rw-r--r--smdpp-data/upp/TS48v5_SAIP2.1A_NoBERTLV.derbin0 -> 12060 bytes
-rw-r--r--smdpp-data/upp/TS48v5_SAIP2.1B_NoBERTLV.derbin0 -> 12140 bytes
-rw-r--r--smdpp-data/upp/TS48v5_SAIP2.3_BERTLV_SUCI.derbin0 -> 12284 bytes
-rw-r--r--smdpp-data/upp/TS48v5_SAIP2.3_NoBERTLV.derbin0 -> 12163 bytes
-rwxr-xr-xsmpp2sim.py41
-rwxr-xr-xsmpp_ota_apdu2.py127
-rwxr-xr-xtests/pySim-prog_test.sh (renamed from tests/pysim-test.sh)7
-rwxr-xr-xtests/pySim-trace_test.sh96
-rw-r--r--tests/test_construct.py66
-rwxr-xr-xtests/test_esim.py64
-rwxr-xr-xtests/test_esim_bsp.py98
-rwxr-xr-xtests/test_esim_saip.py68
-rwxr-xr-xtests/test_euicc.py31
-rwxr-xr-xtests/test_files.py99
-rw-r--r--tests/test_globalplatform.py225
-rw-r--r--tests/test_ota.py4
-rw-r--r--tests/test_sms.py40
-rw-r--r--tests/test_tlv.py1
-rwxr-xr-xtests/test_tlvs.py123
-rwxr-xr-xtests/test_utils.py46
223 files changed, 18230 insertions, 4969 deletions
diff --git a/.checkpatch.conf b/.checkpatch.conf
new file mode 100644
index 0000000..8fdd97e
--- /dev/null
+++ b/.checkpatch.conf
@@ -0,0 +1,2 @@
+--exclude ^pySim/esim/asn1/.*\.asn$
+--exclude ^smdpp-data/.*$
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..7592deb
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+open_collective: osmocom
diff --git a/README.md b/README.md
index 2a89c9a..75f8c44 100644
--- a/README.md
+++ b/README.md
@@ -73,19 +73,20 @@ Installation
Please install the following dependencies:
- - pyscard
- - pyserial
- - pytlv
+ - bidict
- cmd2 >= 1.5.0
- - jsonpath-ng
+ - colorlog
- construct >= 2.9.51
- - bidict
- gsm0338
- - pyyaml >= 5.1
- - termcolor
- - colorlog
+ - jsonpath-ng
- packaging
- pycryptodomex
+ - pyscard
+ - pyserial
+ - pytlv
+ - pyyaml >= 5.1
+ - smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
+ - termcolor
Example for Debian:
```sh
@@ -101,6 +102,9 @@ pip3 install --user -r requirements.txt
After installing all dependencies, the pySim applications ``pySim-read.py``, ``pySim-prog.py`` and ``pySim-shell.py`` may be started directly from the cloned repository.
+In addition to the dependencies above ``pySim-trace.py`` requires ``tshark`` and the python package ``pyshark`` to be installed. It is known that the ``tshark`` package
+in Debian versions before 11 may not work with pyshark.
+
### Archlinux Package
Archlinux users may install the package ``python-pysim-git``
@@ -119,19 +123,34 @@ sudo pacman -Rs python-pysim-git
```
+Forum
+-----
+
+We welcome any pySim related discussions in the
+[SIM Card Technology](https://discourse.osmocom.org/c/sim-card-technology/)
+section of the osmocom discourse (web based Forum).
+
+
Mailing List
------------
There is no separate mailing list for this project. However,
-discussions related to pysim-prog are happening on the
-<openbsc@lists.osmocom.org> mailing list, please see
-<https://lists.osmocom.org/mailman/listinfo/openbsc> for subscription
+discussions related to pySim are happening on the simtrace
+<simtrace@lists.osmocom.org> mailing list, please see
+<https://lists.osmocom.org/mailman/listinfo/simtrace> for subscription
options and the list archive.
Please observe the [Osmocom Mailing List
Rules](https://osmocom.org/projects/cellular-infrastructure/wiki/Mailing_List_Rules)
when posting.
+Issue Tracker
+-------------
+
+We use the [issue tracker of the pysim project on osmocom.org](https://osmocom.org/projects/pysim/issues) for
+tracking the state of bug reports and feature requests. Feel free to submit any issues you may find, or help
+us out by resolving existing issues.
+
Contributing
------------
diff --git a/contrib/csv-encrypt-columns.py b/contrib/csv-encrypt-columns.py
new file mode 100755
index 0000000..2b2bbf3
--- /dev/null
+++ b/contrib/csv-encrypt-columns.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+
+# Utility program to perform column-based encryption of a CSV file holding SIM/UICC
+# related key materials.
+#
+# (C) 2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import csv
+import argparse
+from Cryptodome.Cipher import AES
+
+from pySim.utils import h2b, b2h, Hexstr
+from pySim.card_key_provider import CardKeyProviderCsv
+
+def dict_keys_to_upper(d: dict) -> dict:
+ return {k.upper():v for k,v in d.items()}
+
+class CsvColumnEncryptor:
+ def __init__(self, filename: str, transport_keys: dict):
+ self.filename = filename
+ self.transport_keys = dict_keys_to_upper(transport_keys)
+
+ def encrypt_col(self, colname:str, value: str) -> Hexstr:
+ key = self.transport_keys[colname]
+ cipher = AES.new(h2b(key), AES.MODE_CBC, CardKeyProviderCsv.IV)
+ return b2h(cipher.encrypt(h2b(value)))
+
+ def encrypt(self) -> None:
+ with open(self.filename, 'r') as infile:
+ cr = csv.DictReader(infile)
+ cr.fieldnames = [field.upper() for field in cr.fieldnames]
+
+ with open(self.filename + '.encr', 'w') as outfile:
+ cw = csv.DictWriter(outfile, dialect=csv.unix_dialect, fieldnames=cr.fieldnames)
+ cw.writeheader()
+
+ for row in cr:
+ for key_colname in self.transport_keys:
+ if key_colname in row:
+ row[key_colname] = self.encrypt_col(key_colname, row[key_colname])
+ cw.writerow(row)
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument('CSVFILE', help="CSV file name")
+ parser.add_argument('--csv-column-key', action='append', required=True,
+ help='per-CSV-column AES transport key')
+
+ opts = parser.parse_args()
+
+ csv_column_keys = {}
+ for par in opts.csv_column_key:
+ name, key = par.split(':')
+ csv_column_keys[name] = key
+
+ if len(csv_column_keys) == 0:
+ print("You must specify at least one key!")
+ sys.exit(1)
+
+ csv_column_keys = CardKeyProviderCsv.process_transport_keys(csv_column_keys)
+ for name, key in csv_column_keys.items():
+ print("Encrypting column %s using AES key %s" % (name, key))
+
+ cce = CsvColumnEncryptor(opts.CSVFILE, csv_column_keys)
+ cce.encrypt()
diff --git a/contrib/eidtool.py b/contrib/eidtool.py
new file mode 100755
index 0000000..cd5992b
--- /dev/null
+++ b/contrib/eidtool.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+# Command line tool to compute or verify EID (eUICC ID) values
+#
+# (C) 2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import argparse
+
+from pySim.euicc import compute_eid_checksum, verify_eid_checksum
+
+
+option_parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
+ description="""pySim EID Tool
+This utility program can be used to compute or verify the checksum of an EID
+(eUICC Identifier). See GSMA SGP.29 for the algorithm details.
+
+Example (verification):
+ $ eidtool.py --verify 89882119900000000000000000001654
+ EID checksum verified successfully
+
+Example (generation, passing first 30 digits):
+ $ eidtool.py --compute 898821199000000000000000000016
+ 89882119900000000000000000001654
+
+Example (generation, passing all 32 digits):
+ $ eidtool.py --compute 89882119900000000000000000001600
+ 89882119900000000000000000001654
+
+Example (generation, specifying base 30 digits and number to add):
+ $ eidtool.py --compute 898821199000000000000000000000 --add 16
+ 89882119900000000000000000001654
+""")
+group = option_parser.add_mutually_exclusive_group(required=True)
+group.add_argument('--verify', help='Verify given EID csum')
+group.add_argument('--compute', help='Generate EID csum')
+option_parser.add_argument('--add', type=int, help='Add value to EID base before computing')
+
+
+if __name__ == '__main__':
+ opts = option_parser.parse_args()
+
+ if opts.verify:
+ res = verify_eid_checksum(opts.verify)
+ if res:
+ print("EID checksum verified successfully")
+ sys.exit(0)
+ else:
+ print("EID checksum invalid")
+ sys.exit(1)
+ elif opts.compute:
+ eid = opts.compute
+ if opts.add:
+ if len(eid) != 30:
+ print("EID base must be 30 digits when using --add")
+ sys.exit(2)
+ eid = str(int(eid) + int(opts.add))
+ res = compute_eid_checksum(eid)
+ print(res)
+
diff --git a/contrib/es2p_client.py b/contrib/es2p_client.py
new file mode 100755
index 0000000..1353006
--- /dev/null
+++ b/contrib/es2p_client.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+
+# (C) 2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import copy
+import argparse
+from pySim.esim import es2p
+
+EID_HELP='EID of the eUICC for which eSIM shall be made available'
+ICCID_HELP='The ICCID of the eSIM that shall be made available'
+MATCHID_HELP='MatchingID that shall be used by profile download'
+
+parser = argparse.ArgumentParser(description="""
+Utility to manuall issue requests against the ES2+ API of an SM-DP+ according to GSMA SGP.22.""")
+parser.add_argument('--url', required=True, help='Base URL of ES2+ API endpoint')
+parser.add_argument('--id', required=True, help='Entity identifier passed to SM-DP+')
+parser.add_argument('--client-cert', help='X.509 client certificate used to authenticate to server')
+parser.add_argument('--server-ca-cert', help="""X.509 CA certificates acceptable for the server side. In
+ production use cases, this would be the GSMA Root CA (CI) certificate.""")
+subparsers = parser.add_subparsers(dest='command',help="The command (API function) to call")
+
+parser_dlo = subparsers.add_parser('download-order', help="ES2+ DownloadOrder function")
+parser_dlo.add_argument('--eid', help=EID_HELP)
+parser_dlo.add_argument('--iccid', help=ICCID_HELP)
+parser_dlo.add_argument('--profileType', help='The profile type of which one eSIM shall be made available')
+
+parser_cfo = subparsers.add_parser('confirm-order', help="ES2+ ConfirmOrder function")
+parser_cfo.add_argument('--iccid', required=True, help=ICCID_HELP)
+parser_cfo.add_argument('--eid', help=EID_HELP)
+parser_cfo.add_argument('--matchingId', help=MATCHID_HELP)
+parser_cfo.add_argument('--confirmationCode', help='Confirmation code that shall be used by profile download')
+parser_cfo.add_argument('--smdsAddress', help='SM-DS Address')
+parser_cfo.add_argument('--releaseFlag', action='store_true', help='Shall the profile be immediately released?')
+
+parser_co = subparsers.add_parser('cancel-order', help="ES2+ CancelOrder function")
+parser_co.add_argument('--iccid', required=True, help=ICCID_HELP)
+parser_co.add_argument('--eid', help=EID_HELP)
+parser_co.add_argument('--matchingId', help=MATCHID_HELP)
+parser_co.add_argument('--finalProfileStatusIndicator', required=True, choices=['Available','Unavailable'])
+
+parser_rp = subparsers.add_parser('release-profile', help='ES2+ ReleaseProfile function')
+parser_rp.add_argument('--iccid', required=True, help=ICCID_HELP)
+
+if __name__ == '__main__':
+ opts = parser.parse_args()
+ #print(opts)
+
+ peer = es2p.Es2pApiClient(opts.url, opts.id, server_cert_verify=opts.server_ca_cert, client_cert=opts.client_cert)
+
+ data = {}
+ for k, v in vars(opts).items():
+ if k in ['url', 'id', 'client_cert', 'server_ca_cert', 'command']:
+ # remove keys from dict that shold not end up in JSON...
+ continue
+ if v is not None:
+ data[k] = v
+
+ print(data)
+ if opts.command == 'download-order':
+ res = peer.call_downloadOrder(data)
+ elif opts.command == 'confirm-order':
+ res = peer.call_confirmOrder(data)
+ elif opts.command == 'cancel-order':
+ res = peer.call_cancelOrder(data)
+ elif opts.command == 'release-profile':
+ res = peer.call_releaseProfile(data)
diff --git a/contrib/jenkins.sh b/contrib/jenkins.sh
index 0309877..6fb3d1a 100755
--- a/contrib/jenkins.sh
+++ b/contrib/jenkins.sh
@@ -4,7 +4,7 @@
# environment variables:
# * WITH_MANUALS: build manual PDFs if set to "1"
# * PUBLISH: upload manuals after building if set to "1" (ignored without WITH_MANUALS = "1")
-# * JOB_TYPE: one of 'test', 'pylint', 'docs'
+# * JOB_TYPE: one of 'test', 'distcheck', 'pylint', 'docs'
#
export PYTHONUNBUFFERED=1
@@ -22,13 +22,26 @@ case "$JOB_TYPE" in
. venv/bin/activate
pip install -r requirements.txt
+ pip install pyshark
# Execute automatically discovered unit tests first
python -m unittest discover -v -s tests/
# Run the test with physical cards
cd pysim-testdata
- ../tests/pysim-test.sh
+ ../tests/pySim-prog_test.sh
+ ../tests/pySim-trace_test.sh
+ ;;
+"distcheck")
+ virtualenv -p python3 venv --system-site-packages
+ . venv/bin/activate
+
+ pip install .
+ pip install pyshark
+
+ for prog in venv/bin/pySim-*.py; do
+ $prog --help > /dev/null
+ done
;;
"pylint")
# Print pylint version
@@ -43,7 +56,8 @@ case "$JOB_TYPE" in
--disable E1102 \
--disable E0401 \
--enable W0301 \
- pySim *.py
+ pySim tests/*.py *.py \
+ contrib/es2p_client.py
;;
"docs")
rm -rf docs/_build
diff --git a/contrib/sim-rest-client.py b/contrib/sim-rest-client.py
index 7da2363..a9fabb8 100755
--- a/contrib/sim-rest-client.py
+++ b/contrib/sim-rest-client.py
@@ -162,6 +162,7 @@ def main(argv):
parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
parser.add_argument("-n", "--slot-nr", help="SIM slot number", type=int, default=0)
subp = parser.add_subparsers()
+ subp.required = True
auth_p = subp.add_parser('auth', help='UMTS AKA Authentication')
auth_p.add_argument("-c", "--count", help="Auth count", type=int, default=10)
diff --git a/contrib/sim-rest-server.py b/contrib/sim-rest-server.py
index f2ed63e..0e44b23 100755
--- a/contrib/sim-rest-server.py
+++ b/contrib/sim-rest-server.py
@@ -26,7 +26,10 @@ from klein import Klein
from pySim.transport import ApduTracer
from pySim.transport.pcsc import PcscSimLink
from pySim.commands import SimCardCommands
-from pySim.cards import UsimCard
+from pySim.cards import UiccCardBase
+from pySim.utils import dec_iccid, dec_imsi
+from pySim.ts_51_011 import EF_IMSI
+from pySim.ts_102_221 import EF_ICCID
from pySim.exceptions import *
class ApduPrintTracer(ApduTracer):
@@ -35,18 +38,20 @@ class ApduPrintTracer(ApduTracer):
pass
def connect_to_card(slot_nr:int):
- tp = PcscSimLink(slot_nr, apdu_tracer=ApduPrintTracer())
+ tp = PcscSimLink(argparse.Namespace(pcsc_dev=slot_nr), apdu_tracer=ApduPrintTracer())
tp.connect()
scc = SimCardCommands(tp)
- card = UsimCard(scc)
+ card = UiccCardBase(scc)
# this should be part of UsimCard, but FairewavesSIM breaks with that :/
scc.cla_byte = "00"
scc.sel_ctrl = "0004"
card.read_aids()
- card.select_adf_by_aid(adf='usim')
+
+ # ensure that MF is selected when we are done.
+ card._scc.select_file('3f00')
return tp, scc, card
@@ -96,7 +101,7 @@ class SimRestServer:
elif sw == '6982':
return str(ApiError("Security Status not satisfied - Card PIN enabled?", sw))
else:
- return str(ApiError("Card Communication Error %s" % failure.value), sw)
+ return str(ApiError("Card Communication Error %s" % failure.value, sw))
@app.route('/sim-auth-api/v1/slot/<int:slot>')
@@ -132,10 +137,14 @@ class SimRestServer:
tp, scc, card = connect_to_card(slot)
+ ef_iccid = EF_ICCID()
+ (iccid, sw) = card._scc.read_binary(ef_iccid.fid)
+
card.select_adf_by_aid(adf='usim')
- iccid, sw = card.read_iccid()
- imsi, sw = card.read_imsi()
- res = {"imsi": imsi, "iccid": iccid }
+ ef_imsi = EF_IMSI()
+ (imsi, sw) = card._scc.read_binary(ef_imsi.fid)
+
+ res = {"imsi": dec_imsi(imsi), "iccid": dec_iccid(iccid) }
tp.disconnect()
diff --git a/contrib/unber.py b/contrib/unber.py
new file mode 100755
index 0000000..65262e1
--- /dev/null
+++ b/contrib/unber.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+
+# A more useful verion of the 'unber' tool provided with asn1c:
+# Give a hierarchical decode of BER/DER-encoded ASN.1 TLVs
+
+import sys
+import argparse
+
+from pySim.utils import bertlv_parse_one, bertlv_encode_tag, b2h, h2b
+
+def process_one_level(content: bytes, indent: int):
+ remainder = content
+ while len(remainder):
+ tdict, l, v, remainder = bertlv_parse_one(remainder)
+ #print(tdict)
+ rawtag = bertlv_encode_tag(tdict)
+ if tdict['constructed']:
+ print("%s%s l=%d" % (indent*" ", b2h(rawtag), l))
+ process_one_level(v, indent + 1)
+ else:
+ print("%s%s l=%d %s" % (indent*" ", b2h(rawtag), l, b2h(v)))
+
+
+option_parser = argparse.ArgumentParser(description='BER/DER data dumper')
+group = option_parser.add_mutually_exclusive_group(required=True)
+group.add_argument('--file', help='Input file')
+group.add_argument('--hex', help='Input hexstring')
+
+
+if __name__ == '__main__':
+ opts = option_parser.parse_args()
+
+ if opts.file:
+ with open(opts.file, 'rb') as f:
+ content = f.read()
+ elif opts.hex:
+ content = h2b(opts.hex)
+
+ process_one_level(content, 0)
diff --git a/docs/card-key-provider.rst b/docs/card-key-provider.rst
new file mode 100644
index 0000000..7b4bf2c
--- /dev/null
+++ b/docs/card-key-provider.rst
@@ -0,0 +1,126 @@
+
+Retrieving card-individual keys via CardKeyProvider
+==================================================
+
+When working with a batch of cards, or more than one card in general, it
+is a lot of effort to manually retrieve the card-specific PIN (like
+ADM1) or key material (like SCP02/SCP03 keys).
+
+To increase productivity in that regard, pySim has a concept called the
+`CardKeyProvider`. This is a generic mechanism by which different parts
+of the pySim[-shell] code can programmatically request card-specific key material
+from some data source (*provider*).
+
+For example, when you want to verify the ADM1 PIN using the `verify_adm`
+command without providing an ADM1 value yourself, pySim-shell will
+request the ADM1 value for the ICCID of the card via the
+CardKeyProvider.
+
+There can in theory be multiple different CardKeyProviders. You can for
+example develop your own CardKeyProvider that queries some kind of
+database for the key material, or that uses a key derivation function to
+derive card-specific key material from a global master key.
+
+The only actual CardKeyProvider implementation included in pySim is the
+`CardKeyProviderCsv` which retrieves the key material from a
+[potentially encrypted] CSV file.
+
+
+The CardKeyProviderCsv
+----------------------
+
+The `CardKeyProviderCsv` allows you to retrieve card-individual key
+material from a CSV (comma separated value) file that is accessible to pySim.
+
+The CSV file must have the expected column names, for example `ICCID`
+and `ADM1` in case you would like to use that CSV to obtain the
+card-specific ADM1 PIN when using the `verify_adm` command.
+
+You can specify the CSV file to use via the `--csv` command-line option
+of pySim-shell. If you do not specify a CSV file, pySim will attempt to
+open a CSV file from the default location at
+`~/.osmocom/pysim/card_data.csv`, and use that, if it exists.
+
+Column-Level CSV encryption
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+pySim supports column-level CSV encryption. This feature will make sure
+that your key material is not stored in plaintext in the CSV file.
+
+The encryption mechanism uses AES in CBC mode. You can use any key
+length permitted by AES (128/192/256 bit).
+
+Following GSMA FS.28, the encryption works on column level. This means
+different columns can be decrypted using different key material. This
+means that leakage of a column encryption key for one column or set of
+columns (like a specific security domain) does not compromise various
+other keys that might be stored in other columns.
+
+You can specify column-level decryption keys using the
+`--csv-column-key` command line argument. The syntax is
+`FIELD:AES_KEY_HEX`, for example:
+
+`pySim-shell.py --csv-column-key SCP03_ENC_ISDR:000102030405060708090a0b0c0d0e0f`
+
+In order to avoid having to repeat the column key for each and every
+column of a group of keys within a keyset, there are pre-defined column
+group aliases, which will make sure that the specified key will be used
+by all columns of the set:
+
+* `UICC_SCP02` is a group alias for `UICC_SCP02_KIC1`, `UICC_SCP02_KID1`, `UICC_SCP02_KIK1`
+* `UICC_SCP03` is a group alias for `UICC_SCP03_KIC1`, `UICC_SCP03_KID1`, `UICC_SCP03_KIK1`
+* `SCP03_ECASD` is a group alias for `SCP03_ENC_ECASD`, `SCP03_MAC_ECASD`, `SCP03_DEK_ECASD`
+* `SCP03_ISDA` is a group alias for `SCP03_ENC_ISDA`, `SCP03_MAC_ISDA`, `SCP03_DEK_ISDA`
+* `SCP03_ISDR` is a group alias for `SCP03_ENC_ISDR`, `SCP03_MAC_ISDR`, `SCP03_DEK_ISDR`
+
+
+Field naming
+------------
+
+* For look-up of UICC/SIM/USIM/ISIM or eSIM profile specific key
+ material, pySim uses the `ICCID` field as lookup key.
+
+* For look-up of eUICC specific key material (like SCP03 keys for the
+ ISD-R, ECASD), pySim uses the `EID` field as lookup key.
+
+As soon as the CardKeyProviderCsv finds a line (row) in your CSV where
+the ICCID or EID match, it looks for the column containing the requested
+data.
+
+
+ADM PIN
+~~~~~~~
+
+The `verify_adm` command will attempt to look up the `ADM1` column
+indexed by the ICCID of the SIM/UICC.
+
+
+SCP02 / SCP03
+~~~~~~~~~~~~~
+
+SCP02 and SCP03 each use key triplets consisting if ENC, MAC and DEK
+keys. For more details, see the applicable GlobalPlatform
+specifications.
+
+If you do not want to manually enter the key material for each specific
+card as arguments to the `establish_scp02` or `establish_scp03`
+commands, you can make use of the `--key-provider-suffix` option. pySim
+uses this suffix to compose the column names for the CardKeyProvider as
+follows.
+
+* `SCP02_ENC_` + suffix for the SCP02 ciphering key
+* `SCP02_MAC_` + suffix for the SCP02 MAC key
+* `SCP02_DEK_` + suffix for the SCP02 DEK key
+* `SCP03_ENC_` + suffix for the SCP03 ciphering key
+* `SCP03_MAC_` + suffix for the SCP03 MAC key
+* `SCP03_DEK_` + suffix for the SCP03 DEK key
+
+So for example, if you are using a command like `establish_scp03
+--key-provider-suffix ISDR`, then the column names for the key material
+look-up are `SCP03_ENC_ISDR`, `SCP03_MAC_ISDR` and `SCP03_DEK_ISDR`,
+respectively.
+
+The identifier used for look-up is determined by the definition of the
+Security Domain. For example, the eUICC ISD-R and ECASD will use the EID
+of the eUICC. On the other hand, the ISD-P of an eSIM or the ISD of an
+UICC will use the ICCID.
diff --git a/docs/index.rst b/docs/index.rst
index a46aee0..bcbc7f0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -42,6 +42,7 @@ pySim consists of several parts:
trace
legacy
library
+ osmo-smdpp
Indices and tables
diff --git a/docs/osmo-smdpp.rst b/docs/osmo-smdpp.rst
new file mode 100644
index 0000000..ad7d902
--- /dev/null
+++ b/docs/osmo-smdpp.rst
@@ -0,0 +1,114 @@
+osmo-smdpp
+==========
+
+`osmo-smdpp` is a proof-of-concept implementation of a minimal **SM-DP+** as specified for the *GSMA
+Consumer eSIM Remote SIM provisioning*.
+
+At least at this point, it is intended to be used for research and development, and not as a
+production SM-DP+.
+
+Unless you are a GSMA SAS-SM accredited SM-DP+ operator and have related DPtls, DPauth and DPpb
+certificates signed by the GSMA CI, you **can not use osmo-smdpp with regular production eUICC**.
+This is due to how the GSMA eSIM security architecture works. You can, however, use osmo-smdpp with
+so-called *test-eUICC*, which contain certificates/keys signed by GSMA test certificates as laid out
+in GSMA SGP.26.
+
+At this point, osmo-smdpp does not support anything beyond the bare minimum required to download
+eSIM profiles to an eUICC. Specifically, there is no ES2+ interface, and there is no built-in
+support for profile personalization yet.
+
+osmo-smdpp currently
+
+* uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your osmo-smdppp
+ would be running at the host name `testsmdpplus1.example.com`
+* doesn't understand profile state. Any profile can always be downloaded any number of times, irrespective
+ of the EID or whether it was donwloaded before
+* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical
+* **is absolutely insecure**, as it
+
+ * does not perform any certificate verification
+ * does not evaluate/consider any *Matching ID* or *Confirmation Code*
+ * stores the sessions in an unencrypted _python shelve_ and is hence leaking one-time key materials
+ used for profile encryption and signing.
+
+
+Running osmo-smdpp
+------------------
+
+osmo-smdpp does not have built-in TLS support as the used *twisted* framework appears to have
+problems when using the example elliptic curve certificates (both NIST and Brainpool) from GSMA.
+
+So in order to use it, you have to put it behind a TLS reverse proxy, which terminates the ES9+
+HTTPS from the LPA, and then forwards it as plain HTTP to osmo-smdpp.
+
+nginx as TLS proxy
+~~~~~~~~~~~~~~~~~~
+
+If you use `nginx` as web server, you can use the following configuration snippet::
+
+ upstream smdpp {
+ server localhost:8000;
+ }
+
+ server {
+ listen 443 ssl;
+ server_name testsmdpplus1.example.com;
+
+ ssl_certificate /my/path/to/pysim/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.pem;
+ ssl_certificate_key /my/path/to/pysim/smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_NIST.pem;
+
+ location / {
+ proxy_read_timeout 600s;
+
+ proxy_hide_header X-Powered-By;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_set_header X-Forwarded-Port $proxy_port;
+ proxy_set_header Host $host;
+
+ proxy_pass http://smdpp/;
+ }
+ }
+
+You can of course achieve a similar functionality with apache, lighttpd or many other web server
+software.
+
+
+osmo-smdpp
+~~~~~~~~~~
+
+osmo-smdpp currently doesn't have any configuration file or command line options. You just run it,
+and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
+
+The `smdpp-data/certs`` directory contains the DPtls, DPauth and DPpb as well as CI certificates
+used; they are copied from GSMA SGP.26 v2.
+
+The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used. The file names (without
+.der suffix) are looked up by the matchingID parameter from the activation code presented by the LPA.
+
+
+DNS setup for your LPA
+~~~~~~~~~~~~~~~~~~~~~~
+
+The LPA must resolve `testsmdpplus1.example.com` to the IP address of your TLS proxy.
+
+It must also accept the TLS certificates used by your TLS proxy.
+
+Supported eUICC
+~~~~~~~~~~~~~~~
+
+If you run osmo-smdpp with the included SGP.26 certificates, you must use an eUICC with matching SGP.26
+certificates, i.e. the EUM certificate must be signed by a SGP.26 test root CA and the eUICC certificate
+in turn must be signed by that SGP.26 EUM certificate.
+
+sysmocom (sponsoring development and maintenance of pySim and osmo-smdpp) is selling SGP.26 test eUICC
+as `sysmoEUICC1-C2T`. They are publicly sold in the `sysmocom webshop <https://shop.sysmocom.de/eUICC-for-consumer-eSIM-RSP-with-SGP.26-Test-Certificates/sysmoEUICC1-C2T>`_.
+
+In general you can use osmo-smdpp also with certificates signed by any other certificate authority. You
+just always must ensure that the certificates of the SM-DP+ are signed by the same root CA as those of your
+eUICCs.
+
+Hypothetically, osmo-smdpp could also be operated with GSMA production certificates, but it would require
+that somebody brings the code in-line with all the GSMA security requirements (HSM support, ...) and operate
+it in a GSMA SAS-SM accredited environment and pays for the related audits.
diff --git a/docs/shell.rst b/docs/shell.rst
index ef08fcf..111c502 100644
--- a/docs/shell.rst
+++ b/docs/shell.rst
@@ -1,28 +1,51 @@
pySim-shell
===========
-pySim-shell is an interactive command line shell for all kind of interactions with SIM cards.
+pySim-shell is an interactive command line shell for all kind of interactions with SIM cards,
+including classic GSM SIM, GSM-R SIM, UICC, USIM, ISIM, HPSIM and recently even eUICC.
-The interactive shell provides command for
+If you're familiar with Unix/Linux shells: Think of it like *the bash for SIM cards*.
+
+The pySim-shell interactive shell provides commands for
* navigating the on-card filesystem hierarchy
* authenticating with PINs such as ADM1
* CHV/PIN management (VERIFY, ENABLE, DISABLE, UNBLOCK)
* decoding of SELECT response (file control parameters)
* reading and writing of files and records in raw, hex-encoded binary format
-* for some files where related support has been developed:
+* for most files (where related file-specific encoder/decoder classes have been developed):
- * decoded reading (display file data in JSON format)
+ * decoded reading (display file data represented in human and machine readable JSON format)
* decoded writing (encode from JSON to binary format, then write)
+* if your card supports it, and you have the related privileges: resizing, creating, enabling and disabling of
+ files
+* performing GlobalPlatform operations, including establishment of Secure Channel Protocol (SCP), Installing
+ applications, installing key material, etc.
+* listing/enabling/disabling/deleting eSIM profiles on Consumer eUICC
+
By means of using the python ``cmd2`` module, various useful features improve usability:
* history of commands (persistent across restarts)
* output re-direction to files on your computer
-* output piping through external tools like 'grep'
+* output piping through external tools like ``grep``
* tab completion of commands and SELECT-able files/directories
* interactive help for all commands
+A typical interactive pySim workflow would look like this:
+
+* starting the program, specifying which smart card interface to use to talk to the card
+* verifying the PIN (if needed) or the ADM1 PIN in case you want to write/modify the card
+* selecting on-card application dedicated files like ADF.USIM and navigating the tree of DFs
+* reading and potentially modifying file contents, in raw binary (hex) or decoded JSON format
+
+Video Presentation
+------------------
+
+There is a `video recording of the presentation back when pySim-shell was originally released
+<https://media.ccc.de/v/osmodevcall-20210409-laforge-pysim-shell>`_. While it is slightly dated, it should
+still provide a good introduction.
+
Running pySim-shell
-------------------
@@ -46,11 +69,218 @@ Usage Examples
suci-tutorial
+Advanced Topics
+---------------
+.. toctree::
+ :maxdepth: 1
+ :caption: Advanced pySIM-shell topics
+
+ card-key-provider
+
+
cmd2 basics
-----------
-FIXME
+As pySim-shell is built upon ``cmd2``, some generic cmd2 commands/features are available. You may
+want to check out the `cmd2 Builtin commands <https://cmd2.readthedocs.io/en/stable/features/builtin_commands.html>`_
+to learn about those.
+
+
+pySim commands
+--------------
+
+Commands in this category are pySim specific; they do not have a 1:1 correspondence to ISO 7816
+or 3GPP commands. Mostly they will operate either only on local (in-memory) state, or execute
+a complex sequence of card-commands.
+
+desc
+~~~~
+Display human readable file description for the currently selected file.
+
+
+dir
+~~~
+.. argparse::
+ :module: pySim-shell
+ :func: PySimCommands.dir_parser
+
+Example:
+::
+
+ pySIM-shell (00:MF)> dir
+ MF
+ 3f00
+ .. ADF.USIM DF.SYSTEM EF.DIR EF.UMPC
+ ADF.ARA-M DF.EIRENE DF.TELECOM EF.ICCID MF
+ ADF.ISIM DF.GSM EF.ARR EF.PL
+ 14 files
+
+
+export
+~~~~~~
+.. argparse::
+ :module: pySim-shell
+ :func: PySimCommands.export_parser
+
+Please note that `export` works relative to the current working
+directory, so if you are in `MF`, then the export will contain all known
+files on the card. However, if you are in `ADF.ISIM`, only files below
+that ADF will be part of the export.
+
+Furthermore, it is strongly advised to first enter the ADM1 pin
+(`verify_adm`) to maximize the chance of having permission to read
+all/most files.
+
+
+Example:
+::
+
+ pySIM-shell (00:MF)> export --json > /tmp/export.json
+ EXCEPTION of type 'RuntimeError' occurred with message: 'unable to export 50 elementary file(s) and 2 dedicated file(s), also had to stop early due to exception:6e00: ARA-M - Invalid class'
+ To enable full traceback, run the following command: 'set debug true'
+ pySIM-shell (00:MF)>
+
+The exception above is more or less expected. It just means that 50 files which are defined (most likely as
+optional files in some later 3GPP release) were not found on the card, or were invalidated/disabled when
+trying to SELECT them.
+
+
+tree
+~~~~
+Display a tree of the card filesystem. It is important to note that this displays a tree
+of files that might potentially exist (based on the card profile). In order to determine if
+a given file really exists on a given card, you have to try to select that file.
+
+Example:
+::
+
+ pySIM-shell (00:MF)> tree
+ EF.DIR 2f00 Application Directory
+ EF.ICCID 2fe2 ICC Identification
+ EF.PL 2f05 Preferred Languages
+ EF.ARR 2f06 Access Rule Reference
+ EF.UMPC 2f08 UICC Maximum Power Consumption
+ DF.TELECOM 7f10 None
+ EF.ADN 6f3a Abbreviated Dialing Numbers
+ ...
+
+
+
+verify_adm
+~~~~~~~~~~
+
+.. argparse::
+ :module: pySim-shell
+ :func: PySimCommands.verify_adm_parser
+
+
+Example (successful):
+::
+
+ pySIM-shell (00:MF)> verify_adm 11111111
+ pySIM-shell (00:MF)>
+
+In the above case, the ADM was successfully verified. Please make always sure to use the correct ADM1 for the
+specific card you have inserted! If you present a wrong ADM1 value several times consecutively, your card
+ADM1 will likely be permanently locked, meaning you will never be able to reach ADM1 privilege level.
+For sysmoUSIM/ISIM products, three consecutive wrong ADM1 values will lock the ADM1.
+
+Example (erroneous):
+::
+
+ pySIM-shell (00:MF)> verify_adm 1
+ EXCEPTION of type 'RuntimeError' occurred with message: 'Failed to verify chv_no 0x0A with code 0x31FFFFFFFFFFFFFF, 2 tries left.'
+ To enable full traceback, run the following command: 'set debug true'
+
+If you frequently work with the same set of cards that you need to modify using their ADM1, you can put a CSV
+file with those cards ICCID + ADM1 values into a CSV (comma separated value) file at ``~/.osmocom/pysim/card_data.csv``. In this case,
+you can use the ``verify_adm`` command *without specifying an ADM1 value*.
+
+Example (successful):
+
+::
+
+ pySIM-shell (00:MF)> verify_adm
+ found ADM-PIN '11111111' for ICCID '898821190000000512'
+ pySIM-shell (00:MF)>
+
+In this case, the CSV file contained a record for the ICCID of the card (11111111) and that value was used to
+successfully verify ADM1.
+
+
+Example (erroneous):
+::
+
+ pySIM-shell (00:MF)> verify_adm
+ EXCEPTION of type 'ValueError' occurred with message: 'cannot find ADM-PIN for ICCID '898821190000000512''
+ To enable full traceback, run the following command: 'set debug true'
+
+In this case there was no record for the ICCID of the card in the CSV file.
+
+
+reset
+~~~~~
+Perform card reset and display the card ATR.
+
+Example:
+::
+ pySIM-shell (00:MF)> reset
+ Card ATR: 3b9f96801f878031e073fe211b674a357530350259c4
+ pySIM-shell (00:MF)> reset
+
+
+intro
+~~~~~
+[Re-]Display the introductory banner
+
+Example:
+::
+
+ pySIM-shell (00:MF)> intro
+ Welcome to pySim-shell!
+ (C) 2021-2023 by Harald Welte, sysmocom - s.f.m.c. GmbH and contributors
+ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/shell.html
+
+
+equip
+~~~~~
+Equip pySim-shell with a card; particularly useful if the program was
+started before a card was present, or after a card has been replaced by
+the user while pySim-shell was kept running.
+
+bulk_script
+~~~~~~~~~~~
+.. argparse::
+ :module: pySim-shell
+ :func: PysimApp.bulk_script_parser
+
+
+echo
+~~~~
+.. argparse::
+ :module: pySim-shell
+ :func: PysimApp.echo_parser
+
+
+apdu
+~~~~
+.. argparse::
+ :module: pySim-shell
+ :func: PysimApp.apdu_cmd_parser
+
+Example:
+
+::
+
+ pySIM-shell (00:MF)> apdu 00a40400023f00
+ SW: 6700
+
+In the above case the raw APDU hex-string ``00a40400023f00`` was sent to the card, to which it responded with
+status word ``6700``. Keep in mind that pySim-shell has no idea what kind of raw commands you are sending to the
+card, and it hence is unable to synchronize its internal state (such as the currently selected file) with the
+card. The use of this command should hence be constrained to commands that do not have any high-level support
+in pySim-shell yet.
ISO7816 commands
@@ -68,7 +298,7 @@ Try ``select`` with tab-completion to get a list of all current selectable items
::
- pySIM-shell (MF)> select
+ pySIM-shell (00:MF)> select
.. 2fe2 a0000000871004 EF.ARR MF
2f00 3f00 ADF.ISIM EF.DIR
2f05 7f10 ADF.USIM EF.ICCID
@@ -85,7 +315,7 @@ This will
::
- pySIM-shell (MF)> select ADF.USIM
+ pySIM-shell (00:MF)> select ADF.USIM
{
"file_descriptor": {
"file_descriptor_byte": {
@@ -101,9 +331,13 @@ This will
},
"life_cycle_status_int": "operational_activated",
"security_attrib_compact": "00",
- "pin_status_template_do": "90017083010183018183010A83010B"
+ "pin_status_template_do": {
+ "ps_do": "70",
+ "key_reference": 11
+ }
+
}
- pySIM-shell (MF/ADF.USIM)>
+ pySIM-shell (00:MF/ADF.USIM)>
status
@@ -116,7 +350,7 @@ Example:
::
- pySIM-shell (MF/ADF.ISIM)> status
+ pySIM-shell (00:MF/ADF.ISIM)> status
{
"file_descriptor": {
"file_descriptor_byte": {
@@ -178,7 +412,19 @@ verify_chv
deactivate_file
~~~~~~~~~~~~~~~
-Deactivate the currently selected file. This used to be called INVALIDATE in TS 11.11.
+Deactivate the currently selected file. A deactivated file can no longer be accessed
+for any further operation (such as selecting and subsequently reading or writing).
+
+Any access to a file that is deactivated will trigger the error
+*SW 6283 'Selected file invalidated/disabled'*
+
+In order to re-access a deactivated file, you need to activate it again, see the
+`activate_file` command below. Note that for *deactivation* the to-be-deactivated
+EF must be selected, but for *activation*, the DF above the to-be-activated
+EF must be selected!
+
+This command sends a DEACTIVATE FILE APDU to
+the card (used to be called INVALIDATE in TS 11.11 for classic SIM).
activate_file
@@ -199,6 +445,12 @@ close_channel
:module: pySim-shell
:func: Iso7816Commands.close_chan_parser
+switch_channel
+~~~~~~~~~~~~~~
+.. argparse::
+ :module: pySim-shell
+ :func: Iso7816Commands.switch_chan_parser
+
TS 102 221 commands
-------------------
@@ -233,117 +485,18 @@ sequence including the electrical power down.
:module: pySim.ts_102_221
:func: CardProfileUICC.AddlShellCommands.resume_uicc_parser
+terminal_capability
+~~~~~~~~~~~~~~~~~~~
+This command allows you to perform the TERMINAL CAPABILITY command towards the card.
-pySim commands
---------------
-
-Commands in this category are pySim specific; they do not have a 1:1 correspondence to ISO 7816
-or 3GPP commands. Mostly they will operate either only on local (in-memory) state, or execute
-a complex sequence of card-commands.
-
-desc
-~~~~
-Display human readable file description for the currently selected file.
-
-
-dir
-~~~
-.. argparse::
- :module: pySim-shell
- :func: PySimCommands.dir_parser
-
-Example:
-::
-
- pySIM-shell (MF)> dir
- MF
- 3f00
- .. ADF.USIM DF.SYSTEM EF.DIR EF.UMPC
- ADF.ARA-M DF.EIRENE DF.TELECOM EF.ICCID MF
- ADF.ISIM DF.GSM EF.ARR EF.PL
- 14 files
-
-
-export
-~~~~~~
-.. argparse::
- :module: pySim-shell
- :func: PySimCommands.export_parser
-
-Please note that `export` works relative to the current working
-directory, so if you are in `MF`, then the export will contain all known
-files on the card. However, if you are in `ADF.ISIM`, only files below
-that ADF will be part of the export.
-
-Furthermore, it is strongly advised to first enter the ADM1 pin
-(`verify_adm`) to maximize the chance of having permission to read
-all/most files.
-
-
-tree
-~~~~
-Display a tree of the card filesystem. It is important to note that this displays a tree
-of files that might potentially exist (based on the card profile). In order to determine if
-a given file really exists on a given card, you have to try to select that file.
-
-Example:
-::
-
- pySIM-shell (MF)> tree --help
- EF.DIR 2f00 Application Directory
- EF.ICCID 2fe2 ICC Identification
- EF.PL 2f05 Preferred Languages
- EF.ARR 2f06 Access Rule Reference
- EF.UMPC 2f08 UICC Maximum Power Consumption
- DF.TELECOM 7f10 None
- EF.ADN 6f3a Abbreviated Dialing Numbers
- ...
-
-
-
-verify_adm
-~~~~~~~~~~
-Verify the ADM (Administrator) PIN specified as argument. This is typically needed in order
-to get write/update permissions to most of the files on SIM cards.
-
-Currently only ADM1 is supported.
-
-
-reset
-~~~~~
-Perform card reset and display the card ATR.
-
-intro
-~~~~~
-[Re-]Display the introductory banner
-
-
-equip
-~~~~~
-Equip pySim-shell with a card; particularly useful if the program was
-started before a card was present, or after a card has been replaced by
-the user while pySim-shell was kept running.
-
-bulk_script
-~~~~~~~~~~~
-.. argparse::
- :module: pySim-shell
- :func: PysimApp.bulk_script_parser
-
+TS 102 221 specifies the TERMINAL CAPABILITY command using which the
+terminal (Software + hardware talking to the card) can expose their
+capabilities. This is also used in the eUICC universe to let the eUICC
+know which features are supported.
-echo
-~~~~
.. argparse::
- :module: pySim-shell
- :func: PysimApp.echo_parser
-
-
-apdu
-~~~~
-.. argparse::
- :module: pySim-shell
- :func: PySimCommands.apdu_cmd_parser
-
+ :module: pySim.ts_102_221
+ :func: CardProfileUICC.AddlShellCommands.term_cap_parser
Linear Fixed EF commands
@@ -364,6 +517,9 @@ read_record_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.read_rec_dec_parser
+If this command fails, it means that the record is not decodable, and you should use the :ref:`read_record`
+command and proceed with manual decoding of the contents.
+
read_records
~~~~~~~~~~~~
@@ -378,6 +534,9 @@ read_records_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.read_recs_dec_parser
+If this command fails, it means that the record[s] are not decodable, and you should use the :ref:`read_records`
+command and proceed with manual decoding of the contents.
+
update_record
~~~~~~~~~~~~~
@@ -392,6 +551,9 @@ update_record_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.upd_rec_dec_parser
+If this command fails, it means that the record is not encodable; please check your input and/or use the raw
+:ref:`update_record` command.
+
edit_record_decoded
~~~~~~~~~~~~~~~~~~~
@@ -410,6 +572,12 @@ back to the record on the SIM card.
This allows for easy interactive modification of records.
+If this command fails before the editor is spawned, it means that the current record contents is not decodable,
+and you should use the :ref:`update_record_decoded` or :ref:`update_record` command.
+
+If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
+encodable; please check your input and/or us the raw :ref:`update_record` comamdn.
+
decode_hex
~~~~~~~~~~
@@ -438,6 +606,8 @@ read_binary_decoded
:module: pySim.filesystem
:func: TransparentEF.ShellCommands.read_bin_dec_parser
+If this command fails, it means that the file is not decodable, and you should use the :ref:`read_binary`
+command and proceed with manual decoding of the contents.
update_binary
~~~~~~~~~~~~~
@@ -457,28 +627,43 @@ input. This can be inconvenient if you want to keep 99% of the content but just
parameter. That's where the JSONpath support comes in handy: You can specify a JSONpath to an element
inside the document as well as a new value for tat field:
-Th below example demonstrates this by modifying the ofm field within EF.AD:
+The below example demonstrates this by modifying the ciphering indicator field within EF.AD:
::
- pySIM-shell (MF/ADF.USIM/EF.AD)> read_binary_decoded
+ pySIM-shell (00:MF/ADF.USIM/EF.AD)> read_binary_decoded
+
{
- "ms_operation_mode": "normal",
- "specific_facilities": {
- "ofm": true
+ "ms_operation_mode": "normal_and_specific_facilities",
+ "additional_info": {
+ "ciphering_indicator": false,
+ "csg_display_control": false,
+ "prose_services": false,
+ "extended_drx": true
},
- "len_of_mnc_in_imsi": 2
+ "rfu": 0,
+ "mnc_len": 2,
+ "extensions": "ff"
}
- pySIM-shell (MF/ADF.USIM/EF.AD)> update_binary_decoded --json-path specific_facilities.ofm false
- pySIM-shell (MF/ADF.USIM/EF.AD)> read_binary_decoded
+ pySIM-shell (00:MF/ADF.USIM/EF.AD)> update_binary_decoded --json-path additional_info.ciphering_indicator true
+ "01000902ff"
+ pySIM-shell (00:MF/ADF.USIM/EF.AD)> read_binary_decoded
{
- "ms_operation_mode": "normal",
- "specific_facilities": {
- "ofm": false
+ "ms_operation_mode": "normal_and_specific_facilities",
+ "additional_info": {
+ "ciphering_indicator": true,
+ "csg_display_control": false,
+ "prose_services": false,
+ "extended_drx": true
},
- "len_of_mnc_in_imsi": 2
+ "rfu": 0,
+ "mnc_len": 2,
+ "extensions": "ff"
}
+If this command fails, it means that the file is not encodable; please check your input and/or use the raw
+:ref:`update_binary` command.
+
edit_binary_decoded
~~~~~~~~~~~~~~~~~~~
@@ -493,6 +678,12 @@ to the SIM card.
This allows for easy interactive modification of file contents.
+If this command fails before the editor is spawned, it means that the current file contents is not decodable,
+and you should use the :ref:`update_binary_decoded` or :ref:`update_binary` command.
+
+If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
+encodable; please check your input and/or us the raw :ref:`update_binary` comamdn.
+
decode_hex
~~~~~~~~~~
@@ -725,31 +916,32 @@ it doesn't yet implement fragmentation/reassembly on rule retrieval. YMMV
::
- pySIM-shell (MF/ADF.ARA-M)> aram_get_all
+ pySIM-shell (00:MF/ADF.ARA-M)> aram_get_all
+
[
{
- "ResponseAllRefArDO": [
+ "response_all_ref_ar_do": [
{
- "RefArDO": [
+ "ref_ar_do": [
{
- "RefDO": [
+ "ref_do": [
{
- "AidRefDO": "ffffffffffff"
+ "aid_ref_do": "ffffffffffff"
},
{
- "DevAppIdRefDO": "e46872f28b350b7e1f140de535c2a8d5804f0be3"
+ "dev_app_id_ref_do": "e46872f28b350b7e1f140de535c2a8d5804f0be3"
}
]
},
{
- "ArDO": [
+ "ar_do": [
{
- "ApduArDO": {
+ "apdu_ar_do": {
"generic_access_rule": "always"
}
},
{
- "PermArDO": {
+ "perm_ar_do": {
"permissions": "0000000000000001"
}
}
@@ -777,7 +969,7 @@ aram_store_ref_ar_do
For example, to store an Android UICC carrier privilege rule for the SHA1 hash of the certificate used to sign the CoIMS android app of Supreeth Herle (https://github.com/herlesupreeth/CoIMS_Wiki) you can use the following command:
::
- pySIM-shell (MF/ADF.ARA-M)> aram_store_ref_ar_do --aid FFFFFFFFFFFF --device-app-id E46872F28B350B7E1F140DE535C2A8D5804F0BE3 --android-permissions 0000000000000001 --apdu-always
+ pySIM-shell (00:MF/ADF.ARA-M)> aram_store_ref_ar_do --aid FFFFFFFFFFFF --device-app-id E46872F28B350B7E1F140DE535C2A8D5804F0BE3 --android-permissions 0000000000000001 --apdu-always
aram_delete_all
@@ -795,7 +987,404 @@ projects like GlobalPlatformPro meanwhile.
get_data
~~~~~~~~
-Performs the GET DATA command as specified by GlobalPlatform.
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.get_data_parser
+
+get_status
+~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.get_status_parser
+
+set_status
+~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.set_status_parser
+
+store_data
+~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.store_data_parser
+
+put_key
+~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.put_key_parser
+
+delete_key
+~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.del_key_parser
+
+install_for_personalization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.inst_perso_parser
+
+install_for_install
+~~~~~~~~~~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.inst_inst_parser
+
+delete_card_content
+~~~~~~~~~~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.del_cc_parser
+
+establish_scp02
+~~~~~~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.est_scp02_parser
+
+establish_scp03
+~~~~~~~~~~~~~~~
+.. argparse::
+ :module: pySim.global_platform
+ :func: ADF_SD.AddlShellCommands.est_scp03_parser
+
+release_scp
+~~~~~~~~~~~
+Release any previously established SCP (Secure Channel Protocol)
+
+
+eUICC ISD-R commands
+--------------------
+
+These commands are to perform a variety of operations against eUICC for GSMA consumer eSIM. They
+implement the so-called ES10a, ES10b and ES10c interface. Basically they perform the tasks that usually would
+be done by the LPAd in the UE.
+
+In order to use those commands, you need to go through the specified steps as documented in GSMA SGP.22:
+
+* open a new logical channel (and start to use it)
+* select the ISD-R application
+
+Example::
+
+ pySIM-shell (00:MF)> open_channel 2
+ pySIM-shell (00:MF)> switch_channel 2
+ pySIM-shell (02:MF)> select ADF.ISD-R
+ {
+ "application_id": "a0000005591010ffffffff8900000100",
+ "proprietary_data": {
+ "maximum_length_of_data_field_in_command_message": 255
+ },
+ "isdr_proprietary_application_template": {
+ "supported_version_number": "020200"
+ }
+ }
+ pySIM-shell (02:ADF.ISD-R)>
+
+Once you are at this stage, you can issue the various eUICC related commands against the ISD-R application
+
+
+es10x_store_data
+~~~~~~~~~~~~~~~~
+
+.. argparse::
+ :module: pySim.euicc
+ :func: ADF_ISDR.AddlShellCommands.es10x_store_data_parser
+
+get_euicc_configured_addresses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Obtain the configured SM-DP+ and/or SM-DS addresses using the ES10a GetEuiccConfiguredAddresses() function.
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> get_euicc_configured_addresses
+ {
+ "root_ds_address": "testrootsmds.gsma.com"
+ }
+
+set_default_dp_address
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. argparse::
+ :module: pySim.euicc
+ :func: ADF_ISDR.AddlShellCommands.set_def_dp_addr_parser
+
+get_euicc_challenge
+~~~~~~~~~~~~~~~~~~~
+
+Obtain an authentication challenge from the eUICC using the ES10b GetEUICCChallenge() function.
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> get_euicc_challenge
+ {
+ "euicc_challenge": "3668f20d4e6c8e85609bbca8c14873fd"
+ }
+
+get_euicc_info1
+~~~~~~~~~~~~~~~
+
+Obtain EUICC Information (1) from the eUICC using the ES10b GetEUICCCInfo() function.
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> get_euicc_info1
+ {
+ "svn": "2.2.0",
+ "euicc_ci_pki_list_for_verification": [
+ {
+ "subject_key_identifier_seq": {
+ "unknown_ber_tlv_ie_c0": null
+ }
+ },
+ {
+ "subject_key_identifier_seq": {
+ "unknown_ber_tlv_ie_f5": {
+ "raw": "72bdf98a95d65cbeb88a38a1c11d800a85c3"
+ }
+ }
+ }
+ ],
+ "euicc_ci_pki_list_for_signing": [
+ {
+ "subject_key_identifier_seq": {
+ "unknown_ber_tlv_ie_c0": null
+ }
+ },
+ {
+ "subject_key_identifier_seq": {
+ "unknown_ber_tlv_ie_f5": {
+ "raw": "72bdf98a95d65cbeb88a38a1c11d800a85c3"
+ }
+ }
+ }
+ ]
+ }
+
+
+get_euicc_info2
+~~~~~~~~~~~~~~~
+
+Obtain EUICC Information (2) from the eUICC using the ES10b GetEUICCCInfo() function.
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> get_euicc_info2
+ {
+ "profile_version": "2.1.0",
+ "svn": "2.2.0",
+ "euicc_firmware_ver": "4.4.0",
+ "ext_card_resource": "81010082040006ddc68304000016e0",
+ "uicc_capability": "067f36c0",
+ "ts102241_version": "9.2.0",
+ "global_platform_version": "2.3.0",
+ "rsp_capability": "0490",
+ "euicc_ci_pki_list_for_verification": [
+ {
+ "subject_key_identifier_seq": {
+ "unknown_ber_tlv_ie_c0": null
+ }
+ },
+ {
+ "subject_key_identifier_seq": {
+ "unknown_ber_tlv_ie_f5": {
+ "raw": "72bdf98a95d65cbeb88a38a1c11d800a85c3"
+ }
+ }
+ }
+ ],
+ "euicc_ci_pki_list_for_signing": [
+ {
+ "subject_key_identifier_seq": {
+ "unknown_ber_tlv_ie_c0": null
+ }
+ },
+ {
+ "subject_key_identifier_seq": {
+ "unknown_ber_tlv_ie_f5": {
+ "raw": "72bdf98a95d65cbeb88a38a1c11d800a85c3"
+ }
+ }
+ }
+ ],
+ "unknown_ber_tlv_ie_99": {
+ "raw": "06c0"
+ },
+ "pp_version": "0.0.1",
+ "ss_acreditation_number": "G&DAccreditationNbr",
+ "unknown_ber_tlv_ie_ac": {
+ "raw": "801f312e322e3834302e313233343536372f6d79506c6174666f726d4c6162656c812568747470733a2f2f6d79636f6d70616e792e636f6d2f6d79444c4f41526567697374726172"
+ }
+ }
+
+
+list_notification
+~~~~~~~~~~~~~~~~~
+
+Obtain the list of notifications from the eUICC using the ES10b ListNotification() function.
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> list_notification
+ {
+ "notification_metadata_list": {
+ "notification_metadata": {
+ "seq_number": 61,
+ "profile_mgmt_operation": {
+ "pmo": {
+ "install": true,
+ "enable": false,
+ "disable": false,
+ "delete": false
+ }
+ },
+ "notification_address": "testsmdpplus1.example.com",
+ "iccid": "89000123456789012358"
+ }
+ }
+ }
+
+
+remove_notification_from_list
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. argparse::
+ :module: pySim.euicc
+ :func: ADF_ISDR.AddlShellCommands.rem_notif_parser
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> remove_notification_from_list 60
+ {
+ "delete_notification_status": "ok"
+ }
+
+
+get_profiles_info
+~~~~~~~~~~~~~~~~~
+
+Obtain information about the profiles present on the eUICC using the ES10c GetProfilesInfo() function.
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> get_profiles_info
+ {
+ "profile_info_seq": [
+ {
+ "profile_info": {
+ "iccid": "89000123456789012341",
+ "isdp_aid": "a0000005591010ffffffff8900001100",
+ "profile_state": "disabled",
+ "service_provider_name": "GSMA Test 1A",
+ "profile_name": "GSMA Generic eUICC Test Profile 1A",
+ "profile_class": "operational"
+ }
+ },
+ {
+ "profile_info": {
+ "iccid": "89000123456789012358",
+ "isdp_aid": "a0000005591010ffffffff8900001200",
+ "profile_state": "disabled",
+ "service_provider_name": "OsmocomSPN",
+ "profile_name": "OsmocomProfile",
+ "profile_class": "operational"
+ }
+ }
+ ]
+ }
+
+
+enable_profile
+~~~~~~~~~~~~~~
+
+.. argparse::
+ :module: pySim.euicc
+ :func: ADF_ISDR.AddlShellCommands.en_prof_parser
+
+Example (successful)::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> enable_profile --iccid 89000123456789012358
+ {
+ "enable_result": "ok"
+ }
+
+
+Example (failed attempt enabling a profile that's already enabled)::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> enable_profile --iccid 89000123456789012358
+ {
+ "enable_result": "profileNotInDisabledState"
+ }
+
+disable_profile
+~~~~~~~~~~~~~~~
+
+.. argparse::
+ :module: pySim.euicc
+ :func: ADF_ISDR.AddlShellCommands.dis_prof_parser
+
+Example (successful)::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> disable_profile --iccid 89000123456789012358
+ {
+ "disable_result": "ok"
+ }
+
+delete_profile
+~~~~~~~~~~~~~~
+
+.. argparse::
+ :module: pySim.euicc
+ :func: ADF_ISDR.AddlShellCommands.del_prof_parser
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> delete_profile --iccid 89000123456789012358
+ {
+ "delete_result": "ok"
+ }
+
+
+get_eid
+~~~~~~~
+
+Obtain the EID of the eUICC using the ES10c GetEID() function.
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> get_eid
+ {
+ "eid_value": "89049032123451234512345678901235"
+ }
+
+set_nickname
+~~~~~~~~~~~~
+
+.. argparse::
+ :module: pySim.euicc
+ :func: ADF_ISDR.AddlShellCommands.set_nickname_parser
+
+Example::
+
+ pySIM-shell (00:MF/ADF.ISD-R)> set_nickname --profile-nickname asdf 89000123456789012358
+ {
+ "set_nickname_result": "ok"
+ }
+
+
+get_certs
+~~~~~~~~~
+
+Obtain the certificates from an IoT eUICC using the ES10c GetCerts() function.
+
+get_eim_configuration_data
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Obtain the eIM configuration data from an IoT eUICC using the ES10b GetEimConfigurationData() function.
cmd2 settable parameters
@@ -838,10 +1427,10 @@ Boolean variable that determines if path (e.g. in prompt) is displayed with nume
::
- pySIM-shell (MF/EF.ICCID)> set numeric_path True
+ pySIM-shell (00:MF/EF.ICCID)> set numeric_path True
numeric_path - was: False
now: True
- pySIM-shell (3f00/2fe2)> set numeric_path False
+ pySIM-shell (00:3f00/2fe2)> set numeric_path False
numeric_path - was: True
now: False
- pySIM-shell (MF/EF.ICCID)> help set
+ pySIM-shell (00:MF/EF.ICCID)> help set
diff --git a/docs/suci-tutorial.rst b/docs/suci-tutorial.rst
index fc6e911..b4c950f 100644
--- a/docs/suci-tutorial.rst
+++ b/docs/suci-tutorial.rst
@@ -55,13 +55,13 @@ Launch pySIM:
Using PC/SC reader interface
Autodetected card type: sysmoISIM-SJA2
Welcome to pySim-shell!
- pySIM-shell (MF)>
+ pySIM-shell (00:MF)>
Enter the ADM PIN:
::
- pySIM-shell (MF)> verify_adm XXXXXXXX
+ pySIM-shell (00:MF)> verify_adm XXXXXXXX
Otherwise, write commands will fail with ``SW Mismatch: Expected 9000 and got 6982.``
@@ -70,16 +70,16 @@ Key Provisioning
::
- pySIM-shell (MF)> select MF
- pySIM-shell (MF)> select ADF.USIM
- pySIM-shell (MF/ADF.USIM)> select DF.5GS
- pySIM-shell (MF/ADF.USIM/DF.5GS)> select EF.SUCI_Calc_Info
+ pySIM-shell (00:MF)> select MF
+ pySIM-shell (00:MF)> select ADF.USIM
+ pySIM-shell (00:MF/ADF.USIM)> select DF.5GS
+ pySIM-shell (00:MF/ADF.USIM/DF.5GS)> select EF.SUCI_Calc_Info
By default, the file is present but empty:
::
- pySIM-shell (MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> read_binary_decoded
+ pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> read_binary_decoded
missing Protection Scheme Identifier List data object tag
9000: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -> {}
@@ -106,7 +106,7 @@ Write the config to file (must be single-line input as for now):
::
- pySIM-shell (MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> update_binary_decoded '{ "prot_scheme_id_list": [ {"priority": 0, "identifier": 2, "key_index": 1}, {"priority": 1, "identifier": 1, "key_index": 2}, {"priority": 2, "identifier": 0, "key_index": 0}], "hnet_pubkey_list": [ {"hnet_pubkey_identifier": 27, "hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"}, {"hnet_pubkey_identifier": 30, "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]}'
+ pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> update_binary_decoded '{ "prot_scheme_id_list": [ {"priority": 0, "identifier": 2, "key_index": 1}, {"priority": 1, "identifier": 1, "key_index": 2}, {"priority": 2, "identifier": 0, "key_index": 0}], "hnet_pubkey_list": [ {"hnet_pubkey_identifier": 27, "hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"}, {"hnet_pubkey_identifier": 30, "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]}'
WARNING: These are TEST KEYS with publicly known/specified private keys, and hence unsafe for live/secure
deployments! For use in production networks, you need to generate your own set[s] of keys.
@@ -119,11 +119,11 @@ the contents of the file is **invalid** (ffffffff):
::
- pySIM-shell (MF)> select MF
- pySIM-shell (MF)> select ADF.USIM
- pySIM-shell (MF/ADF.USIM)> select DF.5GS
- pySIM-shell (MF/ADF.USIM/DF.5GS)> select EF.Routing_Indicator
- pySIM-shell (MF/ADF.USIM/DF.5GS/EF.Routing_Indicator)> read_binary_decoded
+ pySIM-shell (00:MF)> select MF
+ pySIM-shell (00:MF)> select ADF.USIM
+ pySIM-shell (00:MF/ADF.USIM)> select DF.5GS
+ pySIM-shell (00:MF/ADF.USIM/DF.5GS)> select EF.Routing_Indicator
+ pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.Routing_Indicator)> read_binary_decoded
9000: ffffffff -> {'raw': 'ffffffff'}
The Routing Indicator is a four-byte file but the actual Routing
@@ -132,7 +132,7 @@ the Routing Indicator to 0x71:
::
- pySIM-shell (MF/ADF.USIM/DF.5GS/EF.Routing_Indicator)> update_binary 17ffffff
+ pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.Routing_Indicator)> update_binary 17ffffff
You can also set the routing indicator to **0x0**, which is *valid* and
means “routing indicator not specifiedâ€, leaving it to the modem.
@@ -144,10 +144,10 @@ First, check out the USIM Service Table (UST):
::
- pySIM-shell (MF)> select MF
- pySIM-shell (MF)> select ADF.USIM
- pySIM-shell (MF/ADF.USIM)> select EF.UST
- pySIM-shell (MF/ADF.USIM/EF.UST)> read_binary_decoded
+ pySIM-shell (00:MF)> select MF
+ pySIM-shell (00:MF)> select ADF.USIM
+ pySIM-shell (00:MF/ADF.USIM)> select EF.UST
+ pySIM-shell (00:MF/ADF.USIM/EF.UST)> read_binary_decoded
9000: beff9f9de73e0408400170730000002e00000000 -> [2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 25, 27, 28, 29, 33, 34, 35, 38, 39, 42, 43, 44, 45, 46, 51, 60, 71, 73, 85, 86, 87, 89, 90, 93, 94, 95, 122, 123, 124, 126]
.. list-table:: From TS31.102
@@ -173,9 +173,9 @@ If you’d like to enable/disable any UST service:
::
- pySIM-shell (MF/ADF.USIM/EF.UST)> ust_service_deactivate 124
- pySIM-shell (MF/ADF.USIM/EF.UST)> ust_service_activate 124
- pySIM-shell (MF/ADF.USIM/EF.UST)> ust_service_deactivate 125
+ pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_deactivate 124
+ pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 124
+ pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_deactivate 125
In this case, UST Service 124 is already enabled and you’re good to go. The
sysmoISIM-SJA2 does not support on-SIM calculation, so service 125 must
diff --git a/osmo-smdpp.py b/osmo-smdpp.py
new file mode 100755
index 0000000..9551396
--- /dev/null
+++ b/osmo-smdpp.py
@@ -0,0 +1,567 @@
+#!/usr/bin/env python3
+
+# Early proof-of-concept towards a SM-DP+ HTTP service for GSMA consumer eSIM RSP
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import sys
+import argparse
+import uuid
+import os
+import functools
+from typing import Optional, Dict, List
+from pprint import pprint as pp
+
+import base64
+from base64 import b64decode
+from klein import Klein
+from twisted.web.iweb import IRequest
+import asn1tools
+
+from pySim.utils import h2b, b2h, swap_nibbles
+
+import pySim.esim.rsp as rsp
+from pySim.esim import saip
+from pySim.esim.es8p import *
+from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id
+from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError
+
+# HACK: make this configurable
+DATA_DIR = './smdpp-data'
+HOSTNAME = 'testsmdpplus1.example.com' # must match certificates!
+
+
+def b64encode2str(req: bytes) -> str:
+ """Encode given input bytes as base64 and return result as string."""
+ return base64.b64encode(req).decode('ascii')
+
+def set_headers(request: IRequest):
+ """Set the request headers as mandatory by GSMA eSIM RSP."""
+ request.setHeader('Content-Type', 'application/json;charset=UTF-8')
+ request.setHeader('X-Admin-Protocol', 'gsma/rsp/v2.1.0')
+
+def build_status_code(subject_code: str, reason_code: str, subject_id: Optional[str], message: Optional[str]) -> Dict:
+ r = {'subjectCode': subject_code, 'reasonCode': reason_code }
+ if subject_id:
+ r['subjectIdentifier'] = subject_id
+ if message:
+ r['message'] = message
+ return r
+
+def build_resp_header(js: dict, status: str = 'Executed-Success', status_code_data = None) -> None:
+ # SGP.22 v3.0 6.5.1.4
+ js['header'] = {
+ 'functionExecutionStatus': {
+ 'status': status,
+ }
+ }
+ if status_code_data:
+ js['header']['functionExecutionStatus']['statusCodeData'] = status_code_data
+
+from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
+from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives import hashes
+from cryptography.exceptions import InvalidSignature
+from cryptography import x509
+
+def ecdsa_tr03111_to_dss(sig: bytes) -> bytes:
+ """convert an ECDSA signature from BSI TR-03111 format to DER: first get long integers; then encode those."""
+ assert len(sig) == 64
+ r = int.from_bytes(sig[0:32], 'big')
+ s = int.from_bytes(sig[32:32*2], 'big')
+ return encode_dss_signature(r, s)
+
+
+class ApiError(Exception):
+ def __init__(self, subject_code: str, reason_code: str, message: Optional[str] = None,
+ subject_id: Optional[str] = None):
+ self.status_code = build_status_code(subject_code, reason_code, subject_id, message)
+
+ def encode(self) -> str:
+ """Encode the API Error into a responseHeader string."""
+ js = {}
+ build_resp_header(js, 'Failed', self.status_code)
+ return json.dumps(js)
+
+class SmDppHttpServer:
+ app = Klein()
+
+ @staticmethod
+ def load_certs_from_path(path: str) -> List[x509.Certificate]:
+ """Load all DER + PEM files from given directory path and return them as list of x509.Certificate
+ instances."""
+ certs = []
+ for dirpath, dirnames, filenames in os.walk(path):
+ for filename in filenames:
+ cert = None
+ if filename.endswith('.der'):
+ with open(os.path.join(dirpath, filename), 'rb') as f:
+ cert = x509.load_der_x509_certificate(f.read())
+ elif filename.endswith('.pem'):
+ with open(os.path.join(dirpath, filename), 'rb') as f:
+ cert = x509.load_pem_x509_certificate(f.read())
+ if cert:
+ # verify it is a CI certificate (keyCertSign + i-rspRole-ci)
+ if not cert_policy_has_oid(cert, oid.id_rspRole_ci):
+ raise ValueError("alleged CI certificate %s doesn't have CI policy" % filename)
+ certs.append(cert)
+ return certs
+
+ def ci_get_cert_for_pkid(self, ci_pkid: bytes) -> Optional[x509.Certificate]:
+ """Find CI certificate for given key identifier."""
+ for cert in self.ci_certs:
+ print("cert: %s" % cert)
+ subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), cert.extensions))
+ print(subject_exts)
+ subject_pkid = subject_exts[0].value
+ print(subject_pkid)
+ if subject_pkid and subject_pkid.key_identifier == ci_pkid:
+ return cert
+ return None
+
+ def __init__(self, server_hostname: str, ci_certs_path: str, use_brainpool: bool = False):
+ self.server_hostname = server_hostname
+ self.upp_dir = os.path.realpath(os.path.join(DATA_DIR, 'upp'))
+ self.ci_certs = self.load_certs_from_path(ci_certs_path)
+ # load DPauth cert + key
+ self.dp_auth = CertAndPrivkey(oid.id_rspRole_dp_auth_v2)
+ cert_dir = os.path.join(DATA_DIR, 'certs')
+ if use_brainpool:
+ self.dp_auth.cert_from_der_file(os.path.join(cert_dir, 'DPauth', 'CERT_S_SM_DPauth_ECDSA_BRP.der'))
+ self.dp_auth.privkey_from_pem_file(os.path.join(cert_dir, 'DPauth', 'SK_S_SM_DPauth_ECDSA_BRP.pem'))
+ else:
+ self.dp_auth.cert_from_der_file(os.path.join(cert_dir, 'DPauth', 'CERT_S_SM_DPauth_ECDSA_NIST.der'))
+ self.dp_auth.privkey_from_pem_file(os.path.join(cert_dir, 'DPauth', 'SK_S_SM_DPauth_ECDSA_NIST.pem'))
+ # load DPpb cert + key
+ self.dp_pb = CertAndPrivkey(oid.id_rspRole_dp_pb_v2)
+ if use_brainpool:
+ self.dp_pb.cert_from_der_file(os.path.join(cert_dir, 'DPpb', 'CERT_S_SM_DPpb_ECDSA_BRP.der'))
+ self.dp_pb.privkey_from_pem_file(os.path.join(cert_dir, 'DPpb', 'SK_S_SM_DPpb_ECDSA_BRP.pem'))
+ else:
+ self.dp_pb.cert_from_der_file(os.path.join(cert_dir, 'DPpb', 'CERT_S_SM_DPpb_ECDSA_NIST.der'))
+ self.dp_pb.privkey_from_pem_file(os.path.join(cert_dir, 'DPpb', 'SK_S_SM_DPpb_ECDSA_NIST.pem'))
+ self.rss = rsp.RspSessionStore(os.path.join(DATA_DIR, "sm-dp-sessions"))
+
+ @app.handle_errors(ApiError)
+ def handle_apierror(self, request: IRequest, failure):
+ request.setResponseCode(200)
+ pp(failure)
+ return failure.value.encode()
+
+ @staticmethod
+ def _ecdsa_verify(cert: x509.Certificate, signature: bytes, data: bytes) -> bool:
+ pubkey = cert.public_key()
+ dss_sig = ecdsa_tr03111_to_dss(signature)
+ try:
+ pubkey.verify(dss_sig, data, ec.ECDSA(hashes.SHA256()))
+ return True
+ except InvalidSignature:
+ return False
+
+ @staticmethod
+ def rsp_api_wrapper(func):
+ """Wrapper that can be used as decorator in order to perform common REST API endpoint entry/exit
+ functionality, such as JSON decoding/encoding and debug-printing."""
+ @functools.wraps(func)
+ def _api_wrapper(self, request: IRequest):
+ # TODO: evaluate User-Agent + X-Admin-Protocol header
+ # TODO: reject any non-JSON Content-type
+
+ content = json.loads(request.content.read())
+ print("Rx JSON: %s" % json.dumps(content))
+ set_headers(request)
+
+ output = func(self, request, content) or {}
+
+ build_resp_header(output)
+ print("Tx JSON: %s" % json.dumps(output))
+ return json.dumps(output)
+ return _api_wrapper
+
+ @app.route('/gsma/rsp2/es9plus/initiateAuthentication', methods=['POST'])
+ @rsp_api_wrapper
+ def initiateAutentication(self, request: IRequest, content: dict) -> dict:
+ """See ES9+ InitiateAuthentication SGP.22 Section 5.6.1"""
+ # Verify that the received address matches its own SM-DP+ address, where the comparison SHALL be
+ # case-insensitive. Otherwise, the SM-DP+ SHALL return a status code "SM-DP+ Address - Refused".
+ if content['smdpAddress'] != self.server_hostname:
+ raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address')
+
+ euiccChallenge = b64decode(content['euiccChallenge'])
+ if len(euiccChallenge) != 16:
+ raise ValueError
+
+ euiccInfo1_bin = b64decode(content['euiccInfo1'])
+ euiccInfo1 = rsp.asn1.decode('EUICCInfo1', euiccInfo1_bin)
+ print("Rx euiccInfo1: %s" % euiccInfo1)
+ #euiccInfo1['svn']
+
+ # TODO: If euiccCiPKIdListForSigningV3 is present ...
+
+ pkid_list = euiccInfo1['euiccCiPKIdListForSigning']
+ if 'euiccCiPKIdListForSigningV3' in euiccInfo1:
+ pkid_list = pkid_list + euiccInfo1['euiccCiPKIdListForSigningV3']
+ # verify it supports one of the keys indicated by euiccCiPKIdListForSigning
+ ci_cert = None
+ for x in pkid_list:
+ ci_cert = self.ci_get_cert_for_pkid(x)
+ # we already support multiple CI certificates but only one set of DPauth + DPpb keys. So we must
+ # make sure we choose a CI key-id which has issued both the eUICC as well as our own SM-DP side
+ # certs.
+ if ci_cert and cert_get_subject_key_id(ci_cert) == self.dp_auth.get_authority_key_identifier().key_identifier:
+ break
+ else:
+ ci_cert = None
+ if not ci_cert:
+ raise ApiError('8.8.2', '3.1', 'None of the proposed Public Key Identifiers is supported by the SM-DP+')
+
+ # TODO: Determine the set of CERT.DPauth.SIG that satisfy the following criteria:
+ # * Part of a certificate chain ending at one of the eSIM CA RootCA Certificate, whose Public Keys is
+ # supported by the eUICC (indicated by euiccCiPKIdListForVerification).
+ # * Using a certificate chain that the eUICC and the LPA both support:
+ #euiccInfo1['euiccCiPKIdListForVerification']
+ # raise ApiError('8.8.4', '3.7', 'The SM-DP+ has no CERT.DPauth.SIG which chains to one of the eSIM CA Root CA CErtificate with a Public Key supported by the eUICC')
+
+ # Generate a TransactionID which is used to identify the ongoing RSP session. The TransactionID
+ # SHALL be unique within the scope and lifetime of each SM-DP+.
+ transactionId = uuid.uuid4().hex.upper()
+ assert not transactionId in self.rss
+
+ # Generate a serverChallenge for eUICC authentication attached to the ongoing RSP session.
+ serverChallenge = os.urandom(16)
+
+ # Generate a serverSigned1 data object as expected by the eUICC and described in section 5.7.13 "ES10b.AuthenticateServer". If and only if both eUICC and LPA indicate crlStaplingV3Support, the SM-DP+ SHALL indicate crlStaplingV3Used in sessionContext.
+ serverSigned1 = {
+ 'transactionId': h2b(transactionId),
+ 'euiccChallenge': euiccChallenge,
+ 'serverAddress': self.server_hostname,
+ 'serverChallenge': serverChallenge,
+ }
+ print("Tx serverSigned1: %s" % serverSigned1)
+ serverSigned1_bin = rsp.asn1.encode('ServerSigned1', serverSigned1)
+ print("Tx serverSigned1: %s" % rsp.asn1.decode('ServerSigned1', serverSigned1_bin))
+ output = {}
+ output['serverSigned1'] = b64encode2str(serverSigned1_bin)
+
+ # Generate a signature (serverSignature1) as described in section 5.7.13 "ES10b.AuthenticateServer" using the SK related to the selected CERT.DPauth.SIG.
+ # serverSignature1 SHALL be created using the private key associated to the RSP Server Certificate for authentication, and verified by the eUICC using the contained public key as described in section 2.6.9. serverSignature1 SHALL apply on serverSigned1 data object.
+ output['serverSignature1'] = b64encode2str(b'\x5f\x37\x40' + self.dp_auth.ecdsa_sign(serverSigned1_bin))
+
+ output['transactionId'] = transactionId
+ server_cert_aki = self.dp_auth.get_authority_key_identifier()
+ output['euiccCiPKIdToBeUsed'] = b64encode2str(b'\x04\x14' + server_cert_aki.key_identifier)
+ output['serverCertificate'] = b64encode2str(self.dp_auth.get_cert_as_der()) # CERT.DPauth.SIG
+ # FIXME: add those certificate
+ #output['otherCertsInChain'] = b64encode2str()
+
+ # create SessionState and store it in rss
+ self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge,
+ cert_get_subject_key_id(ci_cert))
+
+ return output
+
+ @app.route('/gsma/rsp2/es9plus/authenticateClient', methods=['POST'])
+ @rsp_api_wrapper
+ def authenticateClient(self, request: IRequest, content: dict) -> dict:
+ """See ES9+ AuthenticateClient in SGP.22 Section 5.6.3"""
+ transactionId = content['transactionId']
+
+ authenticateServerResp_bin = b64decode(content['authenticateServerResponse'])
+ authenticateServerResp = rsp.asn1.decode('AuthenticateServerResponse', authenticateServerResp_bin)
+ print("Rx %s: %s" % authenticateServerResp)
+ if authenticateServerResp[0] == 'authenticateResponseError':
+ r_err = authenticateServerResp[1]
+ #r_err['transactionId']
+ #r_err['authenticateErrorCode']
+ raise ValueError("authenticateResponseError %s" % r_err)
+
+ r_ok = authenticateServerResp[1]
+ euiccSigned1 = r_ok['euiccSigned1']
+ euiccSigned1_bin = rsp.extract_euiccSigned1(authenticateServerResp_bin)
+ euiccSignature1_bin = r_ok['euiccSignature1']
+ euiccCertificate_dec = r_ok['euiccCertificate']
+ # TODO: use original data, don't re-encode?
+ euiccCertificate_bin = rsp.asn1.encode('Certificate', euiccCertificate_dec)
+ eumCertificate_dec = r_ok['eumCertificate']
+ eumCertificate_bin = rsp.asn1.encode('Certificate', eumCertificate_dec)
+ # TODO v3: otherCertsInChain
+
+ # load certificate
+ euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
+ eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
+
+ # Verify that the transactionId is known and relates to an ongoing RSP session. Otherwise, the SM-DP+
+ # SHALL return a status code "TransactionId - Unknown"
+ ss = self.rss.get(transactionId, None)
+ if ss is None:
+ raise ApiError('8.10.1', '3.9', 'Unknown')
+ ss.euicc_cert = euicc_cert
+ ss.eum_cert = eum_cert # TODO: do we need this in the state?
+
+ # Verify that the Root Certificate of the eUICC certificate chain corresponds to the
+ # euiccCiPKIdToBeUsed or TODO: euiccCiPKIdToBeUsedV3
+ if cert_get_auth_key_id(eum_cert) != ss.ci_cert_id:
+ raise ApiError('8.11.1', '3.9', 'Unknown')
+
+ # Verify the validity of the eUICC certificate chain
+ cs = CertificateSet(self.ci_get_cert_for_pkid(ss.ci_cert_id))
+ cs.add_intermediate_cert(eum_cert)
+ # TODO v3: otherCertsInChain
+ try:
+ cs.verify_cert_chain(euicc_cert)
+ except VerifyError:
+ raise ApiError('8.1.3', '6.1', 'Verification failed (certificate chain)')
+ # raise ApiError('8.1.3', '6.3', 'Expired')
+
+
+ # Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate.
+ # Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed"
+ if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin):
+ raise ApiError('8.1', '6.1', 'Verification failed (euiccSignature1 over euiccSigned1)')
+
+ # TODO: verify EID of eUICC cert is within permitted range of EUM cert
+
+ ss.eid = ss.euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
+ print("EID (from eUICC cert): %s" % ss.eid)
+
+ # Verify that the serverChallenge attached to the ongoing RSP session matches the
+ # serverChallenge returned by the eUICC. Otherwise, the SM-DP+ SHALL return a status code "eUICC -
+ # Verification failed".
+ if euiccSigned1['serverChallenge'] != ss.serverChallenge:
+ raise ApiError('8.1', '6.1', 'Verification failed (serverChallenge)')
+
+ # If ctxParams1 contains a ctxParamsForCommonAuthentication data object, the SM-DP+ Shall [...]
+ # TODO: We really do a very simplistic job here, this needs to be properly implemented later,
+ # considering all the various cases, profile state, etc.
+ if euiccSigned1['ctxParams1'][0] == 'ctxParamsForCommonAuthentication':
+ cpca = euiccSigned1['ctxParams1'][1]
+ matchingId = cpca.get('matchingId', None)
+ if not matchingId:
+ # TODO: check if any pending profile downloads for the EID
+ raise ApiError('8.2.6', '3.8', 'Refused')
+ if matchingId:
+ # look up profile based on matchingID. We simply check if a given file exists for now..
+ path = os.path.join(self.upp_dir, matchingId) + '.der'
+ # prevent directory traversal attack
+ if os.path.commonprefix((os.path.realpath(path),self.upp_dir)) != self.upp_dir:
+ raise ApiError('8.2.6', '3.8', 'Refused')
+ if not os.path.isfile(path) or not os.access(path, os.R_OK):
+ raise ApiError('8.2.6', '3.8', 'Refused')
+ ss.matchingId = matchingId
+ with open(path, 'rb') as f:
+ pes = saip.ProfileElementSequence.from_der(f.read())
+ iccid_str = b2h(pes.get_pe_for_type('header').decoded['iccid'])
+
+ # make pylint happy: E0601: Using variable 'iccid_str' before assignment (used-before-assignment)
+ assert iccid_str
+
+ # FIXME: we actually want to perform the profile binding herr, and read the profile metadat from the profile
+
+ # Put together profileMetadata + _bin
+ ss.profileMetadata = ProfileMetadata(iccid_bin=h2b(swap_nibbles(iccid_str)), spn="OsmocomSPN", profile_name=matchingId)
+ profileMetadata_bin = ss.profileMetadata.gen_store_metadata_request()
+
+ # Put together smdpSigned2 + _bin
+ smdpSigned2 = {
+ 'transactionId': h2b(ss.transactionId),
+ 'ccRequiredFlag': False, # whether the Confirmation Code is required
+ #'bppEuiccOtpk': None, # whether otPK.EUICC.ECKA already used for binding the BPP, tag '5F49'
+ }
+ smdpSigned2_bin = rsp.asn1.encode('SmdpSigned2', smdpSigned2)
+
+ ss.smdpSignature2_do = b'\x5f\x37\x40' + self.dp_pb.ecdsa_sign(smdpSigned2_bin + b'\x5f\x37\x40' + euiccSignature1_bin)
+
+ # update non-volatile state with updated ss object
+ self.rss[transactionId] = ss
+ return {
+ 'transactionId': transactionId,
+ 'profileMetadata': b64encode2str(profileMetadata_bin),
+ 'smdpSigned2': b64encode2str(smdpSigned2_bin),
+ 'smdpSignature2': b64encode2str(ss.smdpSignature2_do),
+ 'smdpCertificate': b64encode2str(self.dp_pb.get_cert_as_der()), # CERT.DPpb.SIG
+ }
+
+ @app.route('/gsma/rsp2/es9plus/getBoundProfilePackage', methods=['POST'])
+ @rsp_api_wrapper
+ def getBoundProfilePackage(self, request: IRequest, content: dict) -> dict:
+ """See ES9+ GetBoundProfilePackage SGP.22 Section 5.6.2"""
+ transactionId = content['transactionId']
+
+ # Verify that the received transactionId is known and relates to an ongoing RSP session
+ ss = self.rss.get(transactionId, None)
+ if not ss:
+ raise ApiError('8.10.1', '3.9', 'The RSP session identified by the TransactionID is unknown')
+
+ prepDownloadResp_bin = b64decode(content['prepareDownloadResponse'])
+ prepDownloadResp = rsp.asn1.decode('PrepareDownloadResponse', prepDownloadResp_bin)
+ print("Rx %s: %s" % prepDownloadResp)
+
+ if prepDownloadResp[0] == 'downloadResponseError':
+ r_err = prepDownloadResp[1]
+ #r_err['transactionId']
+ #r_err['downloadErrorCode']
+ raise ValueError("downloadResponseError %s" % r_err)
+
+ r_ok = prepDownloadResp[1]
+
+ # Verify the euiccSignature2 computed over euiccSigned2 and smdpSignature2 using the PK.EUICC.SIG attached to the ongoing RSP session
+ euiccSigned2 = r_ok['euiccSigned2']
+ euiccSigned2_bin = rsp.extract_euiccSigned2(prepDownloadResp_bin)
+ if not self._ecdsa_verify(ss.euicc_cert, r_ok['euiccSignature2'], euiccSigned2_bin + ss.smdpSignature2_do):
+ raise ApiError('8.1', '6.1', 'eUICC signature is invalid')
+
+ # not in spec: Verify that signed TransactionID is outer transaction ID
+ if h2b(transactionId) != euiccSigned2['transactionId']:
+ raise ApiError('8.10.1', '3.9', 'The signed transactionId != outer transactionId')
+
+ # store otPK.EUICC.ECKA in session state
+ ss.euicc_otpk = euiccSigned2['euiccOtpk']
+ print("euiccOtpk: %s" % (b2h(ss.euicc_otpk)))
+
+ # Generate a one-time ECKA key pair (ot{PK,SK}.DP.ECKA) using the curve indicated by the Key Parameter
+ # Reference value of CERT.DPpb.ECDDSA
+ print("curve = %s" % self.dp_pb.get_curve())
+ ss.smdp_ot = ec.generate_private_key(self.dp_pb.get_curve())
+ # extract the public key in (hopefully) the right format for the ES8+ interface
+ ss.smdp_otpk = ss.smdp_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
+ print("smdpOtpk: %s" % b2h(ss.smdp_otpk))
+ print("smdpOtsk: %s" % b2h(ss.smdp_ot.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())))
+
+ ss.host_id = b'mahlzeit'
+
+ # Generate Session Keys using the CRT, otPK.eUICC.ECKA and otSK.DP.ECKA according to annex G
+ euicc_public_key = ec.EllipticCurvePublicKey.from_encoded_point(ss.smdp_ot.curve, ss.euicc_otpk)
+ ss.shared_secret = ss.smdp_ot.exchange(ec.ECDH(), euicc_public_key)
+ print("shared_secret: %s" % b2h(ss.shared_secret))
+
+ # TODO: Check if this order requires a Confirmation Code verification
+
+ # Perform actual protection + binding of profile package (or return pre-bound one)
+ with open(os.path.join(self.upp_dir, ss.matchingId)+'.der', 'rb') as f:
+ upp = UnprotectedProfilePackage.from_der(f.read(), metadata=ss.profileMetadata)
+ # HACK: Use empty PPP as we're still debuggin the configureISDP step, and we want to avoid
+ # cluttering the log with stuff happening after the failure
+ #upp = UnprotectedProfilePackage.from_der(b'', metadata=ss.profileMetadata)
+ if False:
+ # Use random keys
+ bpp = BoundProfilePackage.from_upp(upp)
+ else:
+ # Use sesssion keys
+ ppp = ProtectedProfilePackage.from_upp(upp, BspInstance(b'\x00'*16, b'\x11'*16, b'\x22'*16))
+ bpp = BoundProfilePackage.from_ppp(ppp)
+
+ # update non-volatile state with updated ss object
+ self.rss[transactionId] = ss
+ return {
+ 'transactionId': transactionId,
+ 'boundProfilePackage': b64encode2str(bpp.encode(ss, self.dp_pb)),
+ }
+
+ @app.route('/gsma/rsp2/es9plus/handleNotification', methods=['POST'])
+ @rsp_api_wrapper
+ def handleNotification(self, request: IRequest, content: dict) -> dict:
+ """See ES9+ HandleNotification in SGP.22 Section 5.6.4"""
+ pendingNotification_bin = b64decode(content['pendingNotification'])
+ pendingNotification = rsp.asn1.decode('PendingNotification', pendingNotification_bin)
+ print("Rx %s: %s" % pendingNotification)
+ if pendingNotification[0] == 'profileInstallationResult':
+ profileInstallRes = pendingNotification[1]
+ pird = profileInstallRes['profileInstallationResultData']
+ transactionId = b2h(pird['transactionId'])
+ ss = self.rss.get(transactionId, None)
+ if ss is None:
+ print("Unable to find session for transactionId")
+ return
+ profileInstallRes['euiccSignPIR']
+ # TODO: use original data, don't re-encode?
+ pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
+ # verify eUICC signature
+ if not self._ecdsa_verify(ss.euicc_cert, profileInstallRes['euiccSignPIR'], pird_bin):
+ print("Unable to verify eUICC signature")
+ print("Profile Installation Final Result: ", pird['finalResult'])
+ # remove session state
+ del self.rss[transactionId]
+ elif pendingNotification[0] == 'otherSignedNotification':
+ # TODO
+ pass
+ else:
+ raise ValueError(pendingNotification)
+
+ #@app.route('/gsma/rsp3/es9plus/handleDeviceChangeRequest, methods=['POST']')
+ #@rsp_api_wrapper
+ #"""See ES9+ ConfirmDeviceChange in SGP.22 Section 5.6.6"""
+ # TODO: implement this
+
+ @app.route('/gsma/rsp2/es9plus/cancelSession', methods=['POST'])
+ @rsp_api_wrapper
+ def cancelSession(self, request: IRequest, content: dict) -> dict:
+ """See ES9+ CancelSession in SGP.22 Section 5.6.5"""
+ print("Rx JSON: %s" % content)
+ transactionId = content['transactionId']
+
+ # Verify that the received transactionId is known and relates to an ongoing RSP session
+ ss = self.rss.get(transactionId, None)
+ if ss is None:
+ raise ApiError('8.10.1', '3.9', 'The RSP session identified by the transactionId is unknown')
+
+ cancelSessionResponse_bin = b64decode(content['cancelSessionResponse'])
+ cancelSessionResponse = rsp.asn1.decode('CancelSessionResponse', cancelSessionResponse_bin)
+ print("Rx %s: %s" % cancelSessionResponse)
+
+ if cancelSessionResponse[0] == 'cancelSessionResponseError':
+ # FIXME: print some error
+ return
+ cancelSessionResponseOk = cancelSessionResponse[1]
+ # TODO: use original data, don't re-encode?
+ ecsr = cancelSessionResponseOk['euiccCancelSessionSigned']
+ ecsr_bin = rsp.asn1.encode('EuiccCancelSessionSigned', ecsr)
+ # Verify the eUICC signature (euiccCancelSessionSignature) using the PK.EUICC.SIG attached to the ongoing RSP session
+ if not self._ecdsa_verify(ss.euicc_cert, cancelSessionResponseOk['euiccCancelSessionSignature'], ecsr_bin):
+ raise ApiError('8.1', '6.1', 'eUICC signature is invalid')
+
+ # Verify that the received smdpOid corresponds to the one in SM-DP+ CERT.DPauth.SIG
+ subj_alt_name = self.dp_auth.get_subject_alt_name()
+ if x509.ObjectIdentifier(ecsr['smdpOid']) != subj_alt_name.oid:
+ raise ApiError('8.8', '3.10', 'The provided SM-DP+ OID is invalid.')
+
+ if ecsr['transactionId'] != h2b(transactionId):
+ raise ApiError('8.10.1', '3.9', 'The signed transactionId != outer transactionId')
+
+ # TODO: 1. Notify the Operator using the function "ES2+.HandleNotification" function
+ # TODO: 2. Terminate the corresponding pending download process.
+ # TODO: 3. If required, execute the SM-DS Event Deletion procedure described in section 3.6.3.
+
+ # delete actual session data
+ del self.rss[transactionId]
+ return { 'transactionId': transactionId }
+
+
+def main(argv):
+ parser = argparse.ArgumentParser()
+ #parser.add_argument("-H", "--host", help="Host/IP to bind HTTP to", default="localhost")
+ #parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000)
+ #parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
+
+ args = parser.parse_args()
+
+ hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=True)
+ #hs.app.run(endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem")
+ hs.app.run("localhost", 8000)
+
+if __name__ == "__main__":
+ main(sys.argv)
diff --git a/ota_test.py b/ota_test.py
index 6ac3f2a..6ffad9d 100755
--- a/ota_test.py
+++ b/ota_test.py
@@ -41,10 +41,29 @@ OTA_KEYSET_SJA5_SAMPLES = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=3,
kic=h2b('300102030405060708090a0b0c0d0e0f'),
kid=h2b('301102030405060708090a0b0c0d0e0f'))
-OTA_KEYSET_SJA5_AES128 = OtaKeyset(algo_crypt='aes_cbc', kic_idx=3,
- algo_auth='aes_cmac', kid_idx=3,
- kic=h2b('300102030405060708090a0b0c0d0e0f'),
- kid=h2b('301102030405060708090a0b0c0d0e0f'))
+OTA_KEYSET_SJA5_AES128 = OtaKeyset(algo_crypt='aes_cbc', kic_idx=2,
+ algo_auth='aes_cmac', kid_idx=2,
+ kic=h2b('200102030405060708090a0b0c0d0e0f'),
+ kid=h2b('201102030405060708090a0b0c0d0e0f'))
+
+# TS.48 profile on sysmoEUICC1-C2G
+OTA_KEYSET_C2G_AES128 = OtaKeyset(algo_crypt='aes_cbc', kic_idx=2,
+ algo_auth='aes_cmac', kid_idx=2,
+ kic=h2b('66778899AABBCCDD1122334455EEFF10'),
+ kid=h2b('112233445566778899AABBCCDDEEFF10'))
+
+
+# ISD-R on sysmoEUICC1-C2G
+OTA_KEYSET_C2G_AES128_ISDR = OtaKeyset(algo_crypt='aes_cbc', kic_idx=1,
+ algo_auth='aes_cmac', kid_idx=1,
+ kic=h2b('B52F9C5938D1C19ED73E1AE772937FD7'),
+ kid=h2b('3BC696ACD1EEC95A6624F7330D22FC81'))
+
+# ISD-A on sysmoEUICC1-C2G
+OTA_KEYSET_C2G_AES128_ISDA = OtaKeyset(algo_crypt='aes_cbc', kic_idx=1,
+ algo_auth='aes_cmac', kid_idx=1,
+ kic=h2b('8DAAD1DAAA8D7C9000E3BBED8B7556E7'),
+ kid=h2b('5392D503AE050DDEAF81AFAEFF275A2B'))
# TODO: AES192
# TODO: AES256
@@ -87,16 +106,17 @@ testcases = [
'encoded_resp': '027100000e0ab0001100000000000000016132',
}
}, {
- 'name': 'AES128-SJA5-CIPHERED-CC',
- 'ota_keyset': OTA_KEYSET_SJA5_AES128,
+ 'name': 'AES128-C2G-CIPHERED-CC',
+ 'ota_keyset': OTA_KEYSET_C2G_AES128_ISDR,
'spi': SPI_CC_POR_CIPHERED_CC,
'request': {
- 'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
- 'encoded_cmd': '00201506193535b00011ae733256918d050b87c94fbfe12e4dc402f262c41cf67f2f',
- 'encoded_tpdu': '400881214365877ff6227052000000000302700000201506193535b00011ae733256918d050b87c94fbfe12e4dc402f262c41cf67f2f',
+ #'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
+ 'apdu': h2b('80ec800300'),
+ 'encoded_cmd': '00281506192222b00011e87cceebb2d93083011ce294f93fc4d8de80da1abae8c37ca3e72ec4432e5058',
+ 'encoded_tpdu': '400881214365877ff6227052000000000302700000281506192222b00011e87cceebb2d93083011ce294f93fc4d8de80da1abae8c37ca3e72ec4432e5058',
},
'response': {
- 'encoded_resp': '027100001c12b000118bb989492c632529326a2f4681feb37c825bc9021c9f6d0b',
+ 'encoded_resp': '027100002412b00011ebc6b497e2cad7aedf36ace0e3a29b38853f0fe9ccde81913be5702b73abce1f',
}
}
]
@@ -109,12 +129,14 @@ for t in testcases:
# RAM: B00000
# SIM RFM: B00010
# USIM RFM: B00011
- tar = h2b('B00011')
+ # ISD-R: 000001
+ # ECASD: 000002
+ tar = h2b('000001')
dialect = OtaDialectSms()
outp = dialect.encode_cmd(od, tar, t['spi'], apdu=t['request']['apdu'])
print("result: %s" % b2h(outp))
- assert(b2h(outp) == t['request']['encoded_cmd'])
+ #assert(b2h(outp) == t['request']['encoded_cmd'])
with_udh = b'\x02\x70\x00' + outp
print("with_udh: %s" % b2h(with_udh))
@@ -125,8 +147,8 @@ for t in testcases:
#tpdu = SMS_SUBMIT(tp_udhi=True, tp_mr=0x23, tp_da=da, tp_pid=0x7F, tp_dcs=0xF6, tp_udl=3, tp_ud=with_udh)
tpdu = SMS_DELIVER(tp_udhi=True, tp_oa=da, tp_pid=0x7F, tp_dcs=0xF6, tp_scts=h2b('22705200000000'), tp_udl=3, tp_ud=with_udh)
print("TPDU: %s" % tpdu)
- print("tpdu: %s" % b2h(tpdu.toBytes()))
- assert(b2h(tpdu.toBytes()) == t['request']['encoded_tpdu'])
+ print("tpdu: %s" % b2h(tpdu.to_bytes()))
+ #assert(b2h(tpdu.to_bytes()) == t['request']['encoded_tpdu'])
r = dialect.decode_resp(od, t['spi'], t['response']['encoded_resp'])
print("RESP: ", r)
diff --git a/pySim-prog.py b/pySim-prog.py
index 7c644b7..db9c327 100755
--- a/pySim-prog.py
+++ b/pySim-prog.py
@@ -25,7 +25,7 @@
#
import hashlib
-from optparse import OptionParser
+import argparse
import os
import random
import re
@@ -35,179 +35,157 @@ import json
import csv
from pySim.commands import SimCardCommands
-from pySim.transport import init_reader
-from pySim.cards import _cards_classes, card_detect
+from pySim.transport import init_reader, argparse_add_reader_args
+from pySim.legacy.cards import _cards_classes, card_detect
from pySim.utils import h2b, swap_nibbles, rpad, derive_milenage_opc, calculate_luhn, dec_iccid
-from pySim.ts_51_011 import EF, EF_AD
+from pySim.ts_51_011 import EF_AD
+from pySim.legacy.ts_51_011 import EF
from pySim.card_handler import *
from pySim.utils import *
def parse_options():
- parser = OptionParser(usage="usage: %prog [options]")
+ parser = argparse.ArgumentParser()
+ argparse_add_reader_args(parser)
- parser.add_option("-d", "--device", dest="device", metavar="DEV",
- help="Serial Device for SIM access [default: %default]",
- default="/dev/ttyUSB0",
- )
- parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
- help="Baudrate used for SIM access [default: %default]",
- default=9600,
- )
- parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", type='int', metavar="PCSC",
- help="Which PC/SC reader number for SIM access",
- default=None,
- )
- parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",
- help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",
- default=None,
- )
- parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",
- help="Baudrate used for modem's port [default: %default]",
- default=115200,
- )
- parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",
- help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",
- default=None,
- )
- parser.add_option("-t", "--type", dest="type",
- help="Card type (user -t list to view) [default: %default]",
+ parser.add_argument("-t", "--type", dest="type",
+ help="Card type (user -t list to view) [default: %(default)s]",
default="auto",
)
- parser.add_option("-T", "--probe", dest="probe",
+ parser.add_argument("-T", "--probe", dest="probe",
help="Determine card type",
default=False, action="store_true"
)
- parser.add_option("-a", "--pin-adm", dest="pin_adm",
+ parser.add_argument("-a", "--pin-adm", dest="pin_adm",
help="ADM PIN used for provisioning (overwrites default)",
)
- parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
+ parser.add_argument("-A", "--pin-adm-hex", dest="pin_adm_hex",
help="ADM PIN used for provisioning, as hex string (16 characters long",
)
- parser.add_option("-e", "--erase", dest="erase", action='store_true',
- help="Erase beforehand [default: %default]",
+ parser.add_argument("-e", "--erase", dest="erase", action='store_true',
+ help="Erase beforehand [default: %(default)s]",
default=False,
)
- parser.add_option("-S", "--source", dest="source",
- help="Data Source[default: %default]",
+ parser.add_argument("-S", "--source", dest="source",
+ help="Data Source[default: %(default)s]",
default="cmdline",
)
# if mode is "cmdline"
- parser.add_option("-n", "--name", dest="name",
- help="Operator name [default: %default]",
+ parser.add_argument("-n", "--name", dest="name",
+ help="Operator name [default: %(default)s]",
default="Magic",
)
- parser.add_option("-c", "--country", dest="country", type="int", metavar="CC",
- help="Country code [default: %default]",
+ parser.add_argument("-c", "--country", dest="country", type=int, metavar="CC",
+ help="Country code [default: %(default)s]",
default=1,
)
- parser.add_option("-x", "--mcc", dest="mcc", type="string",
- help="Mobile Country Code [default: %default]",
+ parser.add_argument("-x", "--mcc", dest="mcc",
+ help="Mobile Country Code [default: %(default)s]",
default="901",
)
- parser.add_option("-y", "--mnc", dest="mnc", type="string",
- help="Mobile Network Code [default: %default]",
+ parser.add_argument("-y", "--mnc", dest="mnc",
+ help="Mobile Network Code [default: %(default)s]",
default="55",
)
- parser.add_option("--mnclen", dest="mnclen", type="choice",
- help="Length of Mobile Network Code [default: %default]",
+ parser.add_argument("--mnclen", dest="mnclen",
+ help="Length of Mobile Network Code [default: %(default)s]",
default="auto",
choices=["2", "3", "auto"],
)
- parser.add_option("-m", "--smsc", dest="smsc",
+ parser.add_argument("-m", "--smsc", dest="smsc",
help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
)
- parser.add_option("-M", "--smsp", dest="smsp",
+ parser.add_argument("-M", "--smsp", dest="smsp",
help="Raw SMSP content in hex [default: auto from SMSC]",
)
- parser.add_option("-s", "--iccid", dest="iccid", metavar="ID",
+ parser.add_argument("-s", "--iccid", dest="iccid", metavar="ID",
help="Integrated Circuit Card ID",
)
- parser.add_option("-i", "--imsi", dest="imsi",
+ parser.add_argument("-i", "--imsi", dest="imsi",
help="International Mobile Subscriber Identity",
)
- parser.add_option("--msisdn", dest="msisdn",
+ parser.add_argument("--msisdn", dest="msisdn",
help="Mobile Subscriber Integrated Services Digital Number",
)
- parser.add_option("-k", "--ki", dest="ki",
+ parser.add_argument("-k", "--ki", dest="ki",
help="Ki (default is to randomize)",
)
- parser.add_option("-o", "--opc", dest="opc",
+ parser.add_argument("-o", "--opc", dest="opc",
help="OPC (default is to randomize)",
)
- parser.add_option("--op", dest="op",
+ parser.add_argument("--op", dest="op",
help="Set OP to derive OPC from OP and KI",
)
- parser.add_option("--acc", dest="acc",
+ parser.add_argument("--acc", dest="acc",
help="Set ACC bits (Access Control Code). not all card types are supported",
)
- parser.add_option("--opmode", dest="opmode", type="choice",
+ parser.add_argument("--opmode", dest="opmode",
help="Set UE Operation Mode in EF.AD (Administrative Data)",
default=None,
choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
)
- parser.add_option("-f", "--fplmn", dest="fplmn", action="append",
+ parser.add_argument("-f", "--fplmn", dest="fplmn", action="append",
help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
)
- parser.add_option("--epdgid", dest="epdgid",
+ parser.add_argument("--epdgid", dest="epdgid",
help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
)
- parser.add_option("--epdgSelection", dest="epdgSelection",
+ parser.add_argument("--epdgSelection", dest="epdgSelection",
help="Set PLMN for ePDG Selection Information. (Only Operator Identifier FQDN format supported)",
)
- parser.add_option("--pcscf", dest="pcscf",
+ parser.add_argument("--pcscf", dest="pcscf",
help="Set Proxy Call Session Control Function (P-CSCF) Address. (Only FQDN format supported)",
)
- parser.add_option("--ims-hdomain", dest="ims_hdomain",
+ parser.add_argument("--ims-hdomain", dest="ims_hdomain",
help="Set IMS Home Network Domain Name in FQDN format",
)
- parser.add_option("--impi", dest="impi",
+ parser.add_argument("--impi", dest="impi",
help="Set IMS private user identity",
)
- parser.add_option("--impu", dest="impu",
+ parser.add_argument("--impu", dest="impu",
help="Set IMS public user identity",
)
- parser.add_option("--read-imsi", dest="read_imsi", action="store_true",
+ parser.add_argument("--read-imsi", dest="read_imsi", action="store_true",
help="Read the IMSI from the CARD", default=False
)
- parser.add_option("--read-iccid", dest="read_iccid", action="store_true",
+ parser.add_argument("--read-iccid", dest="read_iccid", action="store_true",
help="Read the ICCID from the CARD", default=False
)
- parser.add_option("-z", "--secret", dest="secret", metavar="STR",
+ parser.add_argument("-z", "--secret", dest="secret", metavar="STR",
help="Secret used for ICCID/IMSI autogen",
)
- parser.add_option("-j", "--num", dest="num", type=int,
+ parser.add_argument("-j", "--num", dest="num", type=int,
help="Card # used for ICCID/IMSI autogen",
)
- parser.add_option("--batch", dest="batch_mode",
- help="Enable batch mode [default: %default]",
+ parser.add_argument("--batch", dest="batch_mode",
+ help="Enable batch mode [default: %(default)s]",
default=False, action='store_true',
)
- parser.add_option("--batch-state", dest="batch_state", metavar="FILE",
+ parser.add_argument("--batch-state", dest="batch_state", metavar="FILE",
help="Optional batch state file",
)
# if mode is "csv"
- parser.add_option("--read-csv", dest="read_csv", metavar="FILE",
+ parser.add_argument("--read-csv", dest="read_csv", metavar="FILE",
help="Read parameters from CSV file rather than command line")
- parser.add_option("--write-csv", dest="write_csv", metavar="FILE",
+ parser.add_argument("--write-csv", dest="write_csv", metavar="FILE",
help="Append generated parameters in CSV file",
)
- parser.add_option("--write-hlr", dest="write_hlr", metavar="FILE",
+ parser.add_argument("--write-hlr", dest="write_hlr", metavar="FILE",
help="Append generated parameters to OpenBSC HLR sqlite3",
)
- parser.add_option("--dry-run", dest="dry_run",
+ parser.add_argument("--dry-run", dest="dry_run",
help="Perform a 'dry run', don't actually program the card",
default=False, action="store_true")
- parser.add_option("--card_handler", dest="card_handler_config", metavar="FILE",
+ parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
help="Use automatic card handling machine")
- (options, args) = parser.parse_args()
+ options = parser.parse_args()
if options.type == 'list':
for kls in _cards_classes:
@@ -241,9 +219,6 @@ def parse_options():
parser.error(
"Can't give ICCID/IMSI for batch mode, need to use automatic parameters ! see --num and --secret for more information")
- if args:
- parser.error("Extraneous arguments")
-
return options
@@ -357,8 +332,8 @@ def gen_parameters(opts):
# ICCID (19 digits, E.118), though some phase1 vendors use 20 :(
if opts.iccid is not None:
iccid = opts.iccid
- if not _isnum(iccid, 19) and not _isnum(iccid, 20):
- raise ValueError('ICCID must be 19 or 20 digits !')
+ if not _isnum(iccid, 18) and not _isnum(iccid, 19) and not _isnum(iccid, 20):
+ raise ValueError('ICCID must be 18, 19 or 20 digits !')
else:
if opts.num is None:
@@ -566,7 +541,7 @@ def find_row_in_csv_file(csv_file_name:str, num=None, iccid=None, imsi=None):
for row in cr:
# Pick a specific row by line number (num)
if num is not None and iccid is None and imsi is None:
- if opts.num == i:
+ if num == i:
f.close()
return row
@@ -726,7 +701,7 @@ def save_batch(opts):
fh.close()
-def process_card(opts, first, ch):
+def process_card(scc, opts, first, ch):
# Connect transport
ch.get(first)
@@ -748,6 +723,7 @@ def process_card(opts, first, ch):
card.erase()
card.reset()
+ cp = None
# Generate parameters
if opts.source == 'cmdline':
cp = gen_parameters(opts)
@@ -793,8 +769,6 @@ if __name__ == '__main__':
# Init card reader driver
sl = init_reader(opts)
- if sl is None:
- exit(1)
# Create command layer
scc = SimCardCommands(transport=sl)
@@ -820,7 +794,7 @@ if __name__ == '__main__':
while 1:
try:
- rc = process_card(opts, first, ch)
+ rc = process_card(scc, opts, first, ch)
except (KeyboardInterrupt):
print("")
print("Terminated by user!")
diff --git a/pySim-read.py b/pySim-read.py
index f80509c..d34ddc3 100755
--- a/pySim-read.py
+++ b/pySim-read.py
@@ -28,17 +28,20 @@ import os
import random
import re
import sys
-from pySim.ts_51_011 import EF, DF, EF_SST_map, EF_AD
-from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
-from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
+
+from pySim.ts_51_011 import EF_SST_map, EF_AD
+from pySim.legacy.ts_51_011 import EF, DF
+from pySim.ts_31_102 import EF_UST_map
+from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
+from pySim.ts_31_103 import EF_IST_map
+from pySim.legacy.ts_31_103 import EF_ISIM_ADF_map
from pySim.commands import SimCardCommands
from pySim.transport import init_reader, argparse_add_reader_args
from pySim.exceptions import SwMatchError
-from pySim.cards import card_detect, SimCard, UsimCard, IsimCard
-from pySim.utils import h2b, swap_nibbles, rpad, dec_imsi, dec_iccid, dec_msisdn
-from pySim.utils import format_xplmn_w_act, dec_st
-from pySim.utils import h2s, format_ePDGSelection
+from pySim.legacy.cards import card_detect, SimCard, UsimCard, IsimCard
+from pySim.utils import h2b, h2s, swap_nibbles, rpad, dec_imsi, dec_iccid, dec_msisdn
+from pySim.legacy.utils import format_xplmn_w_act, dec_st
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
@@ -71,8 +74,6 @@ if __name__ == '__main__':
# Init card reader driver
sl = init_reader(opts)
- if sl is None:
- exit(1)
# Create command layer
scc = SimCardCommands(transport=sl)
diff --git a/pySim-shell.py b/pySim-shell.py
index 3161844..9acea63 100755
--- a/pySim-shell.py
+++ b/pySim-shell.py
@@ -2,7 +2,7 @@
# Interactive shell for working with SIM / UICC / USIM / ISIM cards
#
-# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
+# (C) 2021-2023 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -41,113 +41,27 @@ import argparse
import os
import sys
+import inspect
from pathlib import Path
from io import StringIO
from pprint import pprint as pp
from pySim.exceptions import *
-from pySim.commands import SimCardCommands
from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
-from pySim.cards import card_detect, SimCard, UsimCard
from pySim.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, bertlv_parse_one, sw_match
-from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr
+from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr, dec_iccid
+from pySim.utils import is_hexstr_or_decimal, is_hexstr, is_decimal
from pySim.card_handler import CardHandler, CardHandlerAuto
-from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel
-from pySim.profile import CardProfile
-from pySim.cdma_ruim import CardProfileRUIM
-from pySim.ts_102_221 import CardProfileUICC
+from pySim.filesystem import CardMF, CardDF, CardADF
from pySim.ts_102_222 import Ts102222Commands
-from pySim.ts_31_102 import CardApplicationUSIM
-from pySim.ts_31_103 import CardApplicationISIM
-from pySim.ts_31_104 import CardApplicationHPSIM
-from pySim.ara_m import CardApplicationARAM
-from pySim.global_platform import CardApplicationISD
from pySim.gsm_r import DF_EIRENE
from pySim.cat import ProactiveCommand
-# we need to import this module so that the SysmocomSJA2 sub-class of
-# CardModel is created, which will add the ATR-based matching and
-# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
-import pySim.sysmocom_sja2
-
from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
-
-def init_card(sl):
- """
- Detect card in reader and setup card profile and runtime state. This
- function must be called at least once on startup. The card and runtime
- state object (rs) is required for all pySim-shell commands.
- """
-
- # Wait up to three seconds for a card in reader and try to detect
- # the card type.
- print("Waiting for card...")
- try:
- sl.wait_for_card(3)
- except NoCardError:
- print("No card detected!")
- return None, None
- except:
- print("Card not readable!")
- return None, None
-
- generic_card = False
- card = card_detect("auto", scc)
- if card is None:
- print("Warning: Could not detect card type - assuming a generic card type...")
- card = SimCard(scc)
- generic_card = True
-
- profile = CardProfile.pick(scc)
- if profile is None:
- print("Unsupported card type!")
- return None, card
-
- # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
- # references, however card manufactures may still decide to pick an
- # arbitrary key reference. In case we run on a generic card class that is
- # detected as an UICC, we will pick the key reference that is officially
- # specified.
- if generic_card and isinstance(profile, CardProfileUICC):
- card._adm_chv_num = 0x0A
-
- print("Info: Card is of type: %s" % str(profile))
-
- # FIXME: This shouln't be here, the profile should add the applications,
- # however, we cannot simply put his into ts_102_221.py since we would
- # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
- # imports from ts_102_221.py. This means we will end up with a circular
- # import, which needs to be resolved first.
- if isinstance(profile, CardProfileUICC):
- profile.add_application(CardApplicationUSIM())
- profile.add_application(CardApplicationISIM())
- profile.add_application(CardApplicationHPSIM())
- profile.add_application(CardApplicationARAM())
- profile.add_application(CardApplicationISD())
- # We have chosen SimCard() above, but we now know it actually is an UICC
- # so it's safe to assume it supports USIM application (which we're adding above).
- # IF we don't do this, we will have a SimCard but try USIM specific commands like
- # the update_ust method (see https://osmocom.org/issues/6055)
- if generic_card:
- card = UsimCard(scc)
-
- # Create runtime state with card profile
- rs = RuntimeState(card, profile)
-
- # FIXME: This is an GSM-R related file, it needs to be added throughout,
- # the profile. At the moment we add it for all cards, this won't hurt,
- # but regular SIM and UICC will not have it and fail to select it.
- rs.mf.add_file(DF_EIRENE())
-
- CardModel.apply_matching_models(scc, rs)
-
- # inform the transport that we can do context-specific SW interpretation
- sl.set_sw_interpreter(rs)
-
- return rs, card
+from pySim.app import init_card
class Cmd2Compat(cmd2.Cmd):
@@ -170,6 +84,9 @@ class Settable2Compat(cmd2.Settable):
class PysimApp(Cmd2Compat):
CUSTOM_CATEGORY = 'pySim Commands'
+ BANNER = """Welcome to pySim-shell!
+(C) 2021-2023 by Harald Welte, sysmocom - s.f.m.c. GmbH and contributors
+Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/shell.html """
def __init__(self, card, rs, sl, ch, script=None):
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
@@ -180,7 +97,7 @@ class PysimApp(Cmd2Compat):
# 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)
- self.intro = style('Welcome to pySim-shell!', fg=RED)
+ self.intro = style(self.BANNER, fg=RED)
self.default_category = 'pySim-shell built-in commands'
self.card = None
self.rs = None
@@ -215,6 +132,10 @@ class PysimApp(Cmd2Compat):
if self.rs:
lchan = self.rs.lchan[0]
lchan.unregister_cmds(self)
+ if self.rs.profile:
+ for cmd_set in self.rs.profile.shell_cmdsets:
+ self.unregister_command_set(cmd_set)
+
for cmds in [Iso7816Commands, Ts102222Commands, PySimCommands]:
cmd_set = self.find_commandsets(cmds)
if cmd_set:
@@ -236,7 +157,13 @@ class PysimApp(Cmd2Compat):
self.register_command_set(Iso7816Commands())
self.register_command_set(Ts102222Commands())
self.register_command_set(PySimCommands())
- self.iccid, sw = self.card.read_iccid()
+
+ try:
+ self.lchan.select('MF/EF.ICCID', self)
+ rs.identity['ICCID'] = dec_iccid(self.lchan.read_binary()[0])
+ except:
+ rs.identity['ICCID'] = None
+
self.lchan.select('MF', self)
rc = True
else:
@@ -269,7 +196,7 @@ class PysimApp(Cmd2Compat):
class Cmd2ApduTracer(ApduTracer):
def __init__(self, cmd2_app):
- self.cmd2 = app
+ self.cmd2 = cmd2_app
def trace_response(self, cmd, sw, resp):
self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
@@ -278,7 +205,11 @@ class PysimApp(Cmd2Compat):
def update_prompt(self):
if self.lchan:
path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
- self.prompt = 'pySIM-shell (%s)> ' % (path_str)
+ scp = self.lchan.scc.scp
+ if scp:
+ self.prompt = 'pySIM-shell (%s:%02u:%s)> ' % (str(scp), self.lchan.lchan_nr, path_str)
+ else:
+ self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, path_str)
else:
if self.card:
self.prompt = 'pySIM-shell (no card profile)> '
@@ -297,23 +228,32 @@ class PysimApp(Cmd2Compat):
@cmd2.with_category(CUSTOM_CATEGORY)
def do_equip(self, opts):
"""Equip pySim-shell with card"""
- if self.rs.profile:
+ if self.rs and self.rs.profile:
for cmd_set in self.rs.profile.shell_cmdsets:
self.unregister_command_set(cmd_set)
- rs, card = init_card(sl)
+ rs, card = init_card(self.sl)
self.equip(card, rs)
apdu_cmd_parser = argparse.ArgumentParser()
- apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
+ apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string')
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
+ apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
@cmd2.with_argparser(apdu_cmd_parser)
def do_apdu(self, opts):
"""Send a raw APDU to the card, and print SW + Response.
- DANGEROUS: pySim-shell will not know any card state changes, and
- not continue to work as expected if you e.g. select a different
- file."""
- data, sw = self.card._scc._tp.send_apdu(opts.APDU)
+ CAUTION: this command bypasses the logical channel handling of pySim-shell and card state changes are not
+ tracked. Dpending on the raw APDU sent, pySim-shell may not continue to work as expected if you e.g. select
+ a different file."""
+
+ # When sending raw APDUs we access the scc object through _scc member of the card object. It should also be
+ # noted that the apdu command plays an exceptional role since it is the only card accessing command that
+ # can be executed without the presence of a runtime state (self.rs) object. However, this also means that
+ # self.lchan is also not present (see method equip).
+ if opts.raw or self.lchan is None:
+ data, sw = self.card._scc.send_apdu(opts.APDU)
+ else:
+ data, sw = self.lchan.scc.send_apdu(opts.APDU)
if data:
self.poutput("SW: %s, RESP: %s" % (sw, data))
else:
@@ -322,6 +262,15 @@ class PysimApp(Cmd2Compat):
if not sw_match(sw, opts.expect_sw):
raise SwMatchError(sw, opts.expect_sw)
+ @cmd2.with_category(CUSTOM_CATEGORY)
+ def do_reset(self, opts):
+ """Reset the Card."""
+ atr = self.card.reset()
+ if self.lchan and self.lchan.scc.scp:
+ self.lchan.scc.scp = None
+ self.poutput('Card ATR: %s' % i2h(atr))
+ self.update_prompt()
+
class InterceptStderr(list):
def __init__(self):
self._stderr_backup = sys.stderr
@@ -364,7 +313,7 @@ class PysimApp(Cmd2Compat):
rc = self.equip(card, rs)
except:
self.poutput("")
- self.poutput("Card initialization failed with an exception:")
+ self.poutput("Card initialization (%s) failed with an exception:" % str(self.sl))
self.poutput("---------------------8<---------------------")
traceback.print_exc()
self.poutput("---------------------8<---------------------")
@@ -433,7 +382,7 @@ class PysimApp(Cmd2Compat):
# In case of failure, try multiple times.
for i in range(opts.tries):
# fetch card into reader bay
- ch.get(first)
+ self.ch.get(first)
# if necessary execute an action before we start processing the card
if(opts.pre_card_action):
@@ -456,9 +405,9 @@ class PysimApp(Cmd2Compat):
# Depending on success or failure, the card goes either in the "error" bin or in the
# "done" bin.
if rc < 0:
- ch.error()
+ self.ch.error()
else:
- ch.done()
+ self.ch.done()
# In most cases it is possible to proceed with the next card, but the
# user may decide to halt immediately when an error occurs
@@ -479,7 +428,7 @@ class PysimApp(Cmd2Compat):
return
except:
self.poutput("")
- self.poutput("Card handling failed with an exception:")
+ self.poutput("Card handling (%s) failed with an exception:" % str(self.sl))
self.poutput("---------------------8<---------------------")
traceback.print_exc()
self.poutput("---------------------8<---------------------")
@@ -492,13 +441,13 @@ class PysimApp(Cmd2Compat):
first = False
echo_parser = argparse.ArgumentParser()
- echo_parser.add_argument('string', help="string to echo on the shell")
+ echo_parser.add_argument('string', help="string to echo on the shell", nargs='+')
@cmd2.with_argparser(echo_parser)
@cmd2.with_category(CUSTOM_CATEGORY)
def do_echo(self, opts):
"""Echo (print) a string on the console"""
- self.poutput(opts.string)
+ self.poutput(' '.join(opts.string))
@cmd2.with_category(CUSTOM_CATEGORY)
def do_version(self, opts):
@@ -552,7 +501,7 @@ class PySimCommands(CommandSet):
if isinstance(self._cmd.lchan.selected_file, CardDF):
if action_df:
- action_df(context, opts)
+ action_df(context, **kwargs)
files = self._cmd.lchan.selected_file.get_selectables(
flags=['FNAMES', 'ANAMES'])
@@ -586,7 +535,32 @@ class PySimCommands(CommandSet):
# below, so we must not move up.
if skip_df == False:
self.walk(indent + 1, action_ef, action_df, context, **kwargs)
- fcp_dec = self._cmd.lchan.select("..", self._cmd)
+
+ parent = self._cmd.lchan.selected_file.parent
+ df = self._cmd.lchan.selected_file
+ adf = self._cmd.lchan.selected_adf
+ if isinstance(parent, CardMF) and (adf and adf.has_fs == False):
+ # Not every application that may be present on a GlobalPlatform card will support the SELECT
+ # command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
+ # SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
+ # Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
+ # "select by name" method, which means we can only select an application and not a file.
+ # The consequence of this is that we may get trapped in an application that does not have
+ # ISIM/USIM like file system support and the only way to leave that application is to select
+ # an ISIM/USIM application in order to get the file system access back.
+ #
+ # To automate this escape-route while traversing the file system we will check whether
+ # the parent file is the MF. When this is the case and the selected ADF has no file system
+ # support, we will select an arbitrary ADF that has file system support first and from there
+ # we will then select the MF.
+ for selectable in parent.get_selectables().items():
+ if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
+ self._cmd.lchan.select(selectable[1].name, self._cmd)
+ break
+ self._cmd.lchan.select(df.get_mf().name, self._cmd)
+ else:
+ # Normal DF/ADF selection
+ fcp_dec = self._cmd.lchan.select("..", self._cmd)
elif action_ef:
df_before_action = self._cmd.lchan.selected_file
@@ -714,8 +688,22 @@ class PySimCommands(CommandSet):
exception_str_add = ""
if opts.filename:
+ # export only that one specified file
self.export_ef(opts.filename, context, **kwargs_export)
else:
+ # export an entire subtree
+ if self._cmd.lchan.selected_file == self._cmd.lchan.selected_file.get_mf():
+ # if we're exporting whole card (from MF), also export application state
+ mf = self._cmd.lchan.selected_file
+ try:
+ for app in self._cmd.rs.profile.applications:
+ app.export(self._cmd, context, **kwargs_export)
+ except Exception as e:
+ print("# Exception %s during export of applications" % (str(e)))
+ print("#")
+ finally:
+ # rewind back to MF
+ self._cmd.lchan.select_file(mf)
try:
self.walk(0, self.export_ef, None, context, **kwargs_export)
except Exception as e:
@@ -745,12 +733,6 @@ class PySimCommands(CommandSet):
raise RuntimeError(
"unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
- def do_reset(self, opts):
- """Reset the Card."""
- atr = self._cmd.card.reset()
- self._cmd.poutput('Card ATR: %s' % i2h(atr))
- self._cmd.update_prompt()
-
def do_desc(self, opts):
"""Display human readable file description for the currently selected file"""
desc = self._cmd.lchan.selected_file.desc
@@ -759,25 +741,31 @@ class PySimCommands(CommandSet):
else:
self._cmd.poutput("no description available")
- def do_verify_adm(self, arg):
- """VERIFY the ADM1 PIN"""
- if arg:
+ verify_adm_parser = argparse.ArgumentParser()
+ verify_adm_parser.add_argument('ADM1', nargs='?', type=is_hexstr_or_decimal,
+ help='ADM1 pin value. If none given, CSV file will be queried')
+
+ @cmd2.with_argparser(verify_adm_parser)
+ def do_verify_adm(self, opts):
+ """Verify the ADM (Administrator) PIN specified as argument. This is typically needed in order
+to get write/update permissions to most of the files on SIM cards.
+
+Currently only ADM1 is supported."""
+ if opts.ADM1:
# use specified ADM-PIN
- pin_adm = sanitize_pin_adm(arg)
+ pin_adm = sanitize_pin_adm(opts.ADM1)
else:
+ iccid = self._cmd.rs.identity['ICCID']
# try to find an ADM-PIN if none is specified
- result = card_key_provider_get_field(
- 'ADM1', key='ICCID', value=self._cmd.iccid)
+ result = card_key_provider_get_field('ADM1', key='ICCID', value=iccid)
pin_adm = sanitize_pin_adm(result)
if pin_adm:
- self._cmd.poutput(
- "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
+ self._cmd.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, iccid))
else:
- raise ValueError(
- "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
+ raise ValueError("cannot find ADM-PIN for ICCID '%s'" % (iccid))
if pin_adm:
- self._cmd.card.verify_adm(h2b(pin_adm))
+ self._cmd.lchan.scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
else:
raise ValueError("error: cannot authenticate, no adm-pin!")
@@ -785,10 +773,13 @@ class PySimCommands(CommandSet):
"""Display information about the currently inserted card"""
self._cmd.poutput("Card info:")
self._cmd.poutput(" Name: %s" % self._cmd.card.name)
- self._cmd.poutput(" ATR: %s" % b2h(self._cmd.card._scc.get_atr()))
- self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
- self._cmd.poutput(" Class-Byte: %s" % self._cmd.card._scc.cla_byte)
- self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.card._scc.sel_ctrl)
+ self._cmd.poutput(" ATR: %s" % self._cmd.rs.identity['ATR'])
+ eid = self._cmd.rs.identity.get('EID', None)
+ if eid:
+ self._cmd.poutput(" EID: %s" % eid)
+ self._cmd.poutput(" ICCID: %s" % self._cmd.rs.identity['ICCID'])
+ self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte)
+ self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl)
self._cmd.poutput(" AIDs:")
for a in self._cmd.rs.mf.applications:
self._cmd.poutput(" %s" % a)
@@ -823,22 +814,20 @@ class Iso7816Commands(CommandSet):
if str(code).upper() not in auto:
return sanitize_pin_adm(code)
- result = card_key_provider_get_field(
- str(code), key='ICCID', value=self._cmd.iccid)
+ iccid = self._cmd.rs.identity['ICCID']
+ result = card_key_provider_get_field(str(code), key='ICCID', value=iccid)
result = sanitize_pin_adm(result)
if result:
- self._cmd.poutput("found %s '%s' for ICCID '%s'" %
- (code.upper(), result, self._cmd.iccid))
+ self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, iccid))
else:
- self._cmd.poutput("cannot find %s for ICCID '%s'" %
- (code.upper(), self._cmd.iccid))
+ self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), iccid))
return result
verify_chv_parser = argparse.ArgumentParser()
verify_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
verify_chv_parser.add_argument(
- 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
+ 'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(verify_chv_parser)
def do_verify_chv(self, opts):
@@ -846,23 +835,23 @@ class Iso7816Commands(CommandSet):
call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
PIN2."""
pin = self.get_code(opts.pin_code)
- (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
+ (data, sw) = self._cmd.lchan.scc.verify_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV verification successful")
unblock_chv_parser = argparse.ArgumentParser()
unblock_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
unblock_chv_parser.add_argument(
- 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
+ 'puk_code', type=is_decimal, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
unblock_chv_parser.add_argument(
- 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
+ 'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(unblock_chv_parser)
def do_unblock_chv(self, opts):
"""Unblock PIN code using specified PUK code"""
new_pin = self.get_code(opts.new_pin_code)
puk = self.get_code(opts.puk_code)
- (data, sw) = self._cmd.card._scc.unblock_chv(
+ (data, sw) = self._cmd.lchan.scc.unblock_chv(
opts.pin_nr, h2b(puk), h2b(new_pin))
self._cmd.poutput("CHV unblock successful")
@@ -870,16 +859,16 @@ class Iso7816Commands(CommandSet):
change_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
change_chv_parser.add_argument(
- 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
+ 'pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
change_chv_parser.add_argument(
- 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
+ 'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(change_chv_parser)
def do_change_chv(self, opts):
"""Change PIN code to a new PIN code"""
new_pin = self.get_code(opts.new_pin_code)
pin = self.get_code(opts.pin_code)
- (data, sw) = self._cmd.card._scc.change_chv(
+ (data, sw) = self._cmd.lchan.scc.change_chv(
opts.pin_nr, h2b(pin), h2b(new_pin))
self._cmd.poutput("CHV change successful")
@@ -887,38 +876,45 @@ class Iso7816Commands(CommandSet):
disable_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
disable_chv_parser.add_argument(
- 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
+ 'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(disable_chv_parser)
def do_disable_chv(self, opts):
"""Disable PIN code using specified PIN code"""
pin = self.get_code(opts.pin_code)
- (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
+ (data, sw) = self._cmd.lchan.scc.disable_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV disable successful")
enable_chv_parser = argparse.ArgumentParser()
enable_chv_parser.add_argument(
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
enable_chv_parser.add_argument(
- 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
+ 'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
@cmd2.with_argparser(enable_chv_parser)
def do_enable_chv(self, opts):
"""Enable PIN code using specified PIN code"""
pin = self.get_code(opts.pin_code)
- (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
+ (data, sw) = self._cmd.lchan.scc.enable_chv(opts.pin_nr, h2b(pin))
self._cmd.poutput("CHV enable successful")
def do_deactivate_file(self, opts):
"""Deactivate the currently selected EF"""
- (data, sw) = self._cmd.card._scc.deactivate_file()
+ (data, sw) = self._cmd.lchan.scc.deactivate_file()
activate_file_parser = argparse.ArgumentParser()
activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
@cmd2.with_argparser(activate_file_parser)
def do_activate_file(self, opts):
- """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
- SIM. You need to specify the name or FID of the file to activate."""
+ """Activate the specified EF by sending an ACTIVATE FILE apdu command (used to be called REHABILITATE
+in TS 11.11 for classic SIM).
+
+This command is used to (re-)activate a file that is currently in deactivated (sometimes also called
+"invalidated") state. You need to call this from the DF above the to-be-activated EF and specify the name or
+FID of the file to activate.
+
+Note that for *deactivation* the to-be-deactivated EF must be selected, but for *activation*, the DF
+above the to-be-activated EF must be selected!"""
(data, sw) = self._cmd.lchan.activate_file(opts.NAME)
def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
@@ -933,8 +929,10 @@ class Iso7816Commands(CommandSet):
@cmd2.with_argparser(open_chan_parser)
def do_open_channel(self, opts):
"""Open a logical channel."""
- (data, sw) = self._cmd.card._scc.manage_channel(
+ (data, sw) = self._cmd.lchan.scc.manage_channel(
mode='open', lchan_nr=opts.chan_nr)
+ # this is executed only in successful case, as unsuccessful raises exception
+ self._cmd.lchan.add_lchan(opts.chan_nr)
close_chan_parser = argparse.ArgumentParser()
close_chan_parser.add_argument(
@@ -943,8 +941,22 @@ class Iso7816Commands(CommandSet):
@cmd2.with_argparser(close_chan_parser)
def do_close_channel(self, opts):
"""Close a logical channel."""
- (data, sw) = self._cmd.card._scc.manage_channel(
+ (data, sw) = self._cmd.lchan.scc.manage_channel(
mode='close', lchan_nr=opts.chan_nr)
+ # this is executed only in successful case, as unsuccessful raises exception
+ self._cmd.rs.del_lchan(opts.chan_nr)
+
+ switch_chan_parser = argparse.ArgumentParser()
+ switch_chan_parser.add_argument(
+ 'chan_nr', type=int, default=0, help='Channel Number')
+
+ @cmd2.with_argparser(switch_chan_parser)
+ def do_switch_channel(self, opts):
+ """Switch currently active logical channel."""
+ self._cmd.lchan._select_pre(self._cmd)
+ self._cmd.lchan = self._cmd.rs.lchan[opts.chan_nr]
+ self._cmd.lchan._select_post(self._cmd)
+ self._cmd.update_prompt()
def do_status(self, opts):
"""Perform the STATUS command."""
@@ -969,6 +981,8 @@ global_group.add_argument('--script', metavar='PATH', default=None,
help='script with pySim-shell commands to be executed automatically at start-up')
global_group.add_argument('--csv', metavar='FILE',
default=None, help='Read card data from CSV file')
+global_group.add_argument('--csv-column-key', metavar='FIELD:AES_KEY_HEX', default=[], action='append',
+ help='per-CSV-column AES transport key')
global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
help="Use automatic card handling machine")
@@ -978,6 +992,11 @@ adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', de
adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
help='ADM PIN used for provisioning, as hex string (16 characters long)')
+option_parser.add_argument("command", nargs='?',
+ help="A pySim-shell command that would optionally be executed at startup")
+option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
+ help="Optional Arguments for command")
+
if __name__ == '__main__':
@@ -990,21 +1009,21 @@ if __name__ == '__main__':
print("Invalid script file!")
sys.exit(2)
+ csv_column_keys = {}
+ for par in opts.csv_column_key:
+ name, key = par.split(':')
+ csv_column_keys[name] = key
+
# Register csv-file as card data provider, either from specified CSV
# or from CSV file in home directory
csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
if opts.csv:
- card_key_provider_register(CardKeyProviderCsv(opts.csv))
+ card_key_provider_register(CardKeyProviderCsv(opts.csv, csv_column_keys))
if os.path.isfile(csv_default):
- card_key_provider_register(CardKeyProviderCsv(csv_default))
+ card_key_provider_register(CardKeyProviderCsv(csv_default, csv_column_keys))
# Init card reader driver
sl = init_reader(opts, proactive_handler = Proact())
- if sl is None:
- exit(1)
-
- # Create command layer
- scc = SimCardCommands(transport=sl)
# Create a card handler (for bulk provisioning)
if opts.card_handler_config:
@@ -1019,7 +1038,7 @@ if __name__ == '__main__':
rs, card = init_card(sl)
app = PysimApp(card, rs, sl, ch, opts.script)
except:
- print("Card initialization failed with an exception:")
+ print("Card initialization (%s) failed with an exception:" % str(sl))
print("---------------------8<---------------------")
traceback.print_exc()
print("---------------------8<---------------------")
@@ -1028,7 +1047,9 @@ if __name__ == '__main__':
" it should also be noted that some readers may behave strangely when no card")
print(" is inserted.)")
print("")
- app = PysimApp(card, None, sl, ch, opts.script)
+ if opts.script:
+ print("will not execute startup script due to card initialization errors!")
+ app = PysimApp(None, None, sl, ch)
# If the user supplies an ADM PIN at via commandline args authenticate
# immediately so that the user does not have to use the shell commands
@@ -1037,8 +1058,11 @@ if __name__ == '__main__':
if not card:
print("Card error, cannot do ADM verification with supplied ADM pin now.")
try:
- card.verify_adm(h2b(pin_adm))
+ card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
except Exception as e:
print(e)
- app.cmdloop()
+ if opts.command:
+ app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
+ else:
+ app.cmdloop()
diff --git a/pySim-trace.py b/pySim-trace.py
index d7e594a..6e5fc93 100755
--- a/pySim-trace.py
+++ b/pySim-trace.py
@@ -6,19 +6,21 @@ import argparse
from pprint import pprint as pp
from pySim.apdu import *
-from pySim.filesystem import RuntimeState
+from pySim.runtime import RuntimeState
-from pySim.cards import UsimCard
+from pySim.cards import UiccCardBase
from pySim.commands import SimCardCommands
from pySim.profile import CardProfile
-from pySim.ts_102_221 import CardProfileUICCSIM
+from pySim.ts_102_221 import CardProfileUICC
from pySim.ts_31_102 import CardApplicationUSIM
from pySim.ts_31_103 import CardApplicationISIM
+from pySim.euicc import CardApplicationISDR, CardApplicationECASD
from pySim.transport import LinkBase
from pySim.apdu_source.gsmtap import GsmtapApduSource
from pySim.apdu_source.pyshark_rspro import PysharkRsproPcap, PysharkRsproLive
from pySim.apdu_source.pyshark_gsmtap import PysharkGsmtapPcap
+from pySim.apdu_source.tca_loader_log import TcaLoaderLogApduSource
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
@@ -37,18 +39,21 @@ ApduCommands = SimApduCommands + UiccApduCommands + UsimApduCommands #+ GpApduCo
class DummySimLink(LinkBase):
"""A dummy implementation of the LinkBase abstract base class. Currently required
- as the UsimCard doesn't work without SimCardCommands, which in turn require
+ as the UiccCardBase doesn't work without SimCardCommands, which in turn require
a LinkBase implementation talking to a card.
In the tracer, we don't actually talk to any card, so we simply drop everything
and claim it is successful.
- The UsimCard / SimCardCommands should be refactored to make this obsolete later."""
+ The UiccCardBase / SimCardCommands should be refactored to make this obsolete later."""
def __init__(self, debug: bool = False, **kwargs):
super().__init__(**kwargs)
self._debug = debug
self._atr = h2i('3B9F96801F878031E073FE211B674A4C753034054BA9')
+ def __str__(self):
+ return "dummy"
+
def _send_apdu_raw(self, pdu):
#print("DummySimLink-apdu: %s" % pdu)
return [], '9000'
@@ -71,34 +76,51 @@ class DummySimLink(LinkBase):
class Tracer:
def __init__(self, **kwargs):
- # we assume a generic SIM + UICC + USIM + ISIM card
- profile = CardProfileUICCSIM()
+ # we assume a generic UICC profile; as all APDUs return 9000 in DummySimLink above,
+ # all CardProfileAddon (including SIM) will probe successful.
+ profile = CardProfileUICC()
profile.add_application(CardApplicationUSIM())
profile.add_application(CardApplicationISIM())
+ profile.add_application(CardApplicationISDR())
+ profile.add_application(CardApplicationECASD())
scc = SimCardCommands(transport=DummySimLink())
- card = UsimCard(scc)
+ card = UiccCardBase(scc)
self.rs = RuntimeState(card, profile)
# APDU Decoder
self.ad = ApduDecoder(ApduCommands)
# parameters
self.suppress_status = kwargs.get('suppress_status', True)
self.suppress_select = kwargs.get('suppress_select', True)
+ self.show_raw_apdu = kwargs.get('show_raw_apdu', False)
self.source = kwargs.get('source', None)
- def format_capdu(self, inst: ApduCommand):
+ def format_capdu(self, apdu: Apdu, inst: ApduCommand):
"""Output a single decoded + processed ApduCommand."""
+ if self.show_raw_apdu:
+ print(apdu)
print("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id, inst.col_sw, inst.processed))
print("===============================")
+ def format_reset(self, apdu: CardReset):
+ """Output a single decoded CardReset."""
+ print(apdu)
+ print("===============================")
+
def main(self):
"""Main loop of tracer: Iterates over all Apdu received from source."""
+ apdu_counter = 0
while True:
# obtain the next APDU from the source (blocking read)
- apdu = self.source.read()
- #print(apdu)
+ try:
+ apdu = self.source.read()
+ apdu_counter = apdu_counter + 1
+ except StopIteration:
+ print("%i APDUs parsed, stop iteration." % apdu_counter)
+ return 0
if isinstance(apdu, CardReset):
self.rs.reset()
+ self.format_reset(apdu)
continue
# ask ApduDecoder to look-up (INS,CLA) + instantiate an ApduCommand derived
@@ -112,8 +134,8 @@ class Tracer:
continue
if self.suppress_status and isinstance(inst, UiccStatus):
continue
- #print(inst)
- self.format_capdu(inst)
+
+ self.format_capdu(apdu, inst)
option_parser = argparse.ArgumentParser(description='Osmocom pySim high-level SIM card trace decoder',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
@@ -128,6 +150,9 @@ global_group.add_argument('--no-suppress-status', action='store_false', dest='su
help="""
Don't suppress displaying STATUS APDUs. We normally suppress them as they don't provide any
information that was not already received in resposne to the most recent SEELCT.""")
+global_group.add_argument('--show-raw-apdu', action='store_true', dest='show_raw_apdu',
+ help="""Show the raw APDU in addition to its parsed form.""")
+
subparsers = option_parser.add_subparsers(help='APDU Source', dest='source', required=True)
@@ -158,11 +183,16 @@ parser_rspro_pyshark_live = subparsers.add_parser('rspro-pyshark-live', help="""
parser_rspro_pyshark_live.add_argument('-i', '--interface', required=True,
help='Name of the network interface to capture on')
+parser_tcaloader_log = subparsers.add_parser('tca-loader-log', help="""
+ Read APDUs from a TCA Loader log file.""")
+parser_tcaloader_log.add_argument('-f', '--log-file', required=True,
+ help='Name of te log file to be read')
+
if __name__ == '__main__':
opts = option_parser.parse_args()
- logger.info('Opening source %s...' % opts.source)
+ logger.info('Opening source %s...', opts.source)
if opts.source == 'gsmtap-udp':
s = GsmtapApduSource(opts.bind_ip, opts.bind_port)
elif opts.source == 'rspro-pyshark-pcap':
@@ -171,8 +201,13 @@ if __name__ == '__main__':
s = PysharkRsproLive(opts.interface)
elif opts.source == 'gsmtap-pyshark-pcap':
s = PysharkGsmtapPcap(opts.pcap_file)
+ elif opts.source == 'tca-loader-log':
+ s = TcaLoaderLogApduSource(opts.log_file)
+ else:
+ raise ValueError("unsupported source %s", opts.source)
- tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select)
+ tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select,
+ show_raw_apdu=opts.show_raw_apdu)
logger.info('Entering main loop...')
tracer.main()
diff --git a/pySim/apdu/__init__.py b/pySim/apdu/__init__.py
index 1d1174d..02ccbad 100644
--- a/pySim/apdu/__init__.py
+++ b/pySim/apdu/__init__.py
@@ -1,4 +1,3 @@
-# coding=utf-8
"""APDU (and TPDU) parser for UICC/USIM/ISIM cards.
The File (and its classes) represent the structure / hierarchy
@@ -10,7 +9,7 @@ is far too simplistic, while this decoder can utilize all of the information
we already know in pySim about the filesystem structure, file encoding, etc.
"""
-# (C) 2022 by Harald Welte <laforge@osmocom.org>
+# (C) 2022-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -27,15 +26,16 @@ we already know in pySim about the filesystem structure, file encoding, etc.
import abc
-from termcolor import colored
import typing
from typing import List, Dict, Optional
+from termcolor import colored
-from construct import *
+from construct import Byte, GreedyBytes
from construct import Optional as COptional
+
from pySim.construct import *
from pySim.utils import *
-from pySim.filesystem import RuntimeLchan, RuntimeState, lchan_nr_from_cla
+from pySim.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
"""There are multiple levels of decode:
@@ -52,8 +52,8 @@ from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
class ApduCommandMeta(abc.ABCMeta):
"""A meta-class that we can use to set some class variables when declaring
a derived class of ApduCommand."""
- def __new__(metacls, name, bases, namespace, **kwargs):
- x = super().__new__(metacls, name, bases, namespace)
+ def __new__(mcs, name, bases, namespace, **kwargs):
+ x = super().__new__(mcs, name, bases, namespace)
x._name = namespace.get('name', kwargs.get('n', None))
x._ins = namespace.get('ins', kwargs.get('ins', None))
x._cla = namespace.get('cla', kwargs.get('cla', None))
@@ -152,6 +152,8 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
_construct_p2 = Byte
_construct = HexAdapter(GreedyBytes)
_construct_rsp = HexAdapter(GreedyBytes)
+ _tlv = None
+ _tlv_rsp = None
def __init__(self, cmd: BytesOrHex, rsp: Optional[BytesOrHex] = None):
"""Instantiate a new ApduCommand from give cmd + resp."""
@@ -187,44 +189,39 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
if apdu_case in [1, 2]:
# data is part of response
return cls(buffer[:5], buffer[5:])
- elif apdu_case in [3, 4]:
+ if apdu_case in [3, 4]:
# data is part of command
lc = buffer[4]
return cls(buffer[:5+lc], buffer[5+lc:])
- else:
- raise ValueError('%s: Invalid APDU Case %u' % (cls.__name__, apdu_case))
+ raise ValueError('%s: Invalid APDU Case %u' % (cls.__name__, apdu_case))
@property
def path(self) -> List[str]:
"""Return (if known) the path as list of files to the file on which this command operates."""
if self.file:
return self.file.fully_qualified_path()
- else:
- return []
+ return []
@property
def path_str(self) -> str:
"""Return (if known) the path as string to the file on which this command operates."""
if self.file:
return self.file.fully_qualified_path_str()
- else:
- return ''
+ return ''
@property
def col_sw(self) -> str:
"""Return the ansi-colorized status word. Green==OK, Red==Error"""
if self.successful:
return colored(b2h(self.sw), 'green')
- else:
- return colored(b2h(self.sw), 'red')
+ return colored(b2h(self.sw), 'red')
@property
def lchan_nr(self) -> int:
"""Logical channel number over which this ApduCommand was transmitted."""
if self.lchan:
return self.lchan.lchan_nr
- else:
- return lchan_nr_from_cla(self.cla)
+ return lchan_nr_from_cla(self.cla)
def __str__(self) -> str:
return '%02u %s(%s): %s' % (self.lchan_nr, type(self).__name__, self.path_str, self.to_dict())
@@ -236,7 +233,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
"""Fall-back function to be called if there is no derived-class-specific
process_global or process_on_lchan method. Uses information from APDU decode."""
self.processed = {}
- if not 'p1' in self.cmd_dict:
+ if 'p1' not in self.cmd_dict:
self.processed = self.to_dict()
else:
self.processed['p1'] = self.cmd_dict['p1']
@@ -304,7 +301,12 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
r['p2'] = parse_construct(self._construct_p2, self.p2.to_bytes(1, 'big'))
r['p3'] = self.p3
if self.cmd_data:
- r['body'] = parse_construct(self._construct, self.cmd_data)
+ if self._tlv:
+ ie = self._tlv()
+ ie.from_tlv(self.cmd_data)
+ r['body'] = ie.to_dict()
+ else:
+ r['body'] = parse_construct(self._construct, self.cmd_data)
return r
def rsp_to_dict(self) -> Dict:
@@ -315,7 +317,12 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
else:
r = {}
if self.rsp_data:
- r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
+ if self._tlv_rsp:
+ ie = self._tlv_rsp()
+ ie.from_tlv(self.rsp_data)
+ r['body'] = ie.to_dict()
+ else:
+ r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
r['sw'] = b2h(self.sw)
return r
@@ -428,8 +435,7 @@ class TpduFilter(ApduHandler):
apdu = Apdu(icmd, tpdu.rsp)
if self.apdu_handler:
return self.apdu_handler.input(apdu)
- else:
- return Apdu(icmd, tpdu.rsp)
+ return Apdu(icmd, tpdu.rsp)
def input(self, cmd: bytes, rsp: bytes):
if isinstance(cmd, str):
@@ -448,4 +454,10 @@ class ApduDecoder(ApduHandler):
class CardReset:
- pass
+ def __init__(self, atr: bytes):
+ self.atr = atr
+
+ def __str__(self):
+ if self.atr:
+ return '%s(%s)' % (type(self).__name__, b2h(self.atr))
+ return '%s' % (type(self).__name__)
diff --git a/pySim/apdu/ts_102_221.py b/pySim/apdu/ts_102_221.py
index cd246b1..eaac868 100644
--- a/pySim/apdu/ts_102_221.py
+++ b/pySim/apdu/ts_102_221.py
@@ -1,7 +1,7 @@
# coding=utf-8
"""APDU definitions/decoders of ETSI TS 102 221, the core UICC spec.
-(C) 2022 by Harald Welte <laforge@osmocom.org>
+(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -17,11 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
+from typing import Optional, Dict
import logging
+
+from construct import GreedyRange, Struct
+
from pySim.construct import *
from pySim.filesystem import *
+from pySim.runtime import RuntimeLchan
from pySim.apdu import ApduCommand, ApduCommandSet
-from typing import Optional, Dict, Tuple
+from pySim.utils import i2h
+from pySim import cat
logger = logging.getLogger(__name__)
@@ -76,7 +82,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
pass
# iterate to next element in path
continue
- logger.warning('SELECT UNKNOWN FID %s (%s)' % (file_hex, '/'.join([b2h(x) for x in path])))
+ logger.warning('SELECT UNKNOWN FID %s (%s)', file_hex, '/'.join([b2h(x) for x in path]))
elif mode == 'df_ef_or_mf_by_file_id':
if len(self.cmd_data) != 2:
raise ValueError('Expecting a 2-byte FID')
@@ -90,7 +96,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
#print("\tSELECT %s FAILED" % sels[file_hex])
pass
else:
- logger.warning('SELECT UNKNOWN FID %s' % (file_hex))
+ logger.warning('SELECT UNKNOWN FID %s', file_hex)
elif mode == 'df_name':
# Select by AID (can be sub-string!)
aid = self.cmd_dict['body']
@@ -101,8 +107,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
lchan.selected_file = lchan.selected_adf
#print("\tSELECT AID %s" % adf)
else:
- logger.warning('SELECT UNKNOWN AID %s' % aid)
- pass
+ logger.warning('SELECT UNKNOWN AID %s', aid)
else:
raise ValueError('Select Mode %s not implemented' % mode)
# decode the SELECT response
@@ -289,12 +294,9 @@ class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
@staticmethod
def _pin_is_success(sw):
- if sw[0] == 0x63:
- return True
- else:
- return False
+ return bool(sw[0] == 0x63)
- def process_on_lchan(self, lchan: RuntimeLchan):
+ def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -306,7 +308,7 @@ class ChangePin(ApduCommand, n='CHANGE PIN', ins=0x24, cla=['0X', '4X', '6X']):
_apdu_case = 3
_construct_p2 = PinConstructP2
- def process_on_lchan(self, lchan: RuntimeLchan):
+ def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -318,7 +320,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
_apdu_case = 3
_construct_p2 = PinConstructP2
- def process_on_lchan(self, lchan: RuntimeLchan):
+ def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -329,7 +331,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
class EnablePin(ApduCommand, n='ENABLE PIN', ins=0x28, cla=['0X', '4X', '6X']):
_apdu_case = 3
_construct_p2 = PinConstructP2
- def process_on_lchan(self, lchan: RuntimeLchan):
+ def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -341,7 +343,7 @@ class UnblockPin(ApduCommand, n='UNBLOCK PIN', ins=0x2C, cla=['0X', '4X', '6X'])
_apdu_case = 3
_construct_p2 = PinConstructP2
- def process_on_lchan(self, lchan: RuntimeLchan):
+ def process_on_lchan(self, _lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -394,13 +396,12 @@ class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X',
manage_channel.add_lchan(created_channel_nr)
self.col_id = '%02u' % created_channel_nr
return {'mode': mode, 'created_channel': created_channel_nr }
- elif mode == 'close_channel':
- closed_channel_nr = self.cmd_dict['p2']
+ if mode == 'close_channel':
+ closed_channel_nr = self.cmd_dict['p2']['logical_channel_number']
rs.del_lchan(closed_channel_nr)
self.col_id = '%02u' % closed_channel_nr
return {'mode': mode, 'closed_channel': closed_channel_nr }
- else:
- raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
+ raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
# TS 102 221 Section 11.1.18
class GetChallenge(ApduCommand, n='GET CHALLENGE', ins=0x84, cla=['0X', '4X', '6X']):
@@ -418,13 +419,13 @@ class ManageSecureChannel(ApduCommand, n='MANAGE SECURE CHANNEL', ins=0x73, cla=
p2 = hdr[3]
if p1 & 0x7 == 0: # retrieve UICC Endpoints
return 2
- elif p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
+ if p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
p2_cmd = p2 >> 5
if p2_cmd in [0,2,4]: # command data
return 3
- elif p2_cmd in [1,3,5]: # response data
+ if p2_cmd in [1,3,5]: # response data
return 2
- elif p1 & 0xf == 4: # terminate secure channel SA
+ if p1 & 0xf == 4: # terminate secure channel SA
return 3
raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
@@ -435,8 +436,7 @@ class TransactData(ApduCommand, n='TRANSACT DATA', ins=0x75, cla=['0X', '4X', '6
p1 = hdr[2]
if p1 & 0x04:
return 3
- else:
- return 2
+ return 2
# TS 102 221 Section 11.1.22
class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
@@ -459,14 +459,17 @@ class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['80']):
# TS 102 221 Section 11.2.2 / TS 102 223
class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
_apdu_case = 4
+ _tlv = cat.EventCollection
# TS 102 221 Section 11.2.3 / TS 102 223
class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
_apdu_case = 2
+ _tlv_rsp = cat.ProactiveCommand
# TS 102 221 Section 11.2.3 / TS 102 223
class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
_apdu_case = 3
+ _tlv = cat.TerminalResponse
# TS 102 221 Section 11.3.1
class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
@@ -489,7 +492,7 @@ class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'E
elif self.p2 & 0xdf == 0x40:
c['mode'] = 'retransmit_previous_block'
else:
- logger.warning('%s: invalid P2=%02x' % (self, self.p2))
+ logger.warning('%s: invalid P2=%02x', self, self.p2)
return c
def _decode_cmd(self):
diff --git a/pySim/apdu/ts_31_102.py b/pySim/apdu/ts_31_102.py
index 2200388..c335773 100644
--- a/pySim/apdu/ts_31_102.py
+++ b/pySim/apdu/ts_31_102.py
@@ -9,12 +9,12 @@ APDU commands of 3GPP TS 31.102 V16.6.0
"""
from typing import Dict
-from construct import *
+from construct import BitStruct, Enum, BitsInteger, Int8ub, Bytes, this, Struct, If, Switch, Const
from construct import Optional as COptional
+
from pySim.filesystem import *
from pySim.construct import *
from pySim.ts_31_102 import SUCI_TlvDataObject
-
from pySim.apdu import ApduCommand, ApduCommandSet
# Copyright (C) 2022 Harald Welte <laforge@osmocom.org>
@@ -35,8 +35,6 @@ from pySim.apdu import ApduCommand, ApduCommandSet
# Mapping between USIM Service Number and its description
-from pySim.apdu import ApduCommand, ApduCommandSet
-
# TS 31.102 Section 7.1
class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '4X', '6X']):
_apdu_case = 4
diff --git a/pySim/apdu/ts_51_011.py b/pySim/apdu/ts_51_011.py
index b527f4d..e738ea5 100644
--- a/pySim/apdu/ts_51_011.py
+++ b/pySim/apdu/ts_51_011.py
@@ -18,8 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
+from construct import GreedyRange, Struct
from pySim.construct import *
from pySim.filesystem import *
+from pySim.runtime import RuntimeLchan
from pySim.apdu import ApduCommand, ApduCommandSet
from typing import Optional, Dict, Tuple
diff --git a/pySim/apdu_source/__init__.py b/pySim/apdu_source/__init__.py
index 098d3ca..99df865 100644
--- a/pySim/apdu_source/__init__.py
+++ b/pySim/apdu_source/__init__.py
@@ -14,7 +14,6 @@ class ApduSource(abc.ABC):
@abc.abstractmethod
def read_packet(self) -> PacketType:
"""Read one packet from the source."""
- pass
def read(self) -> Union[Apdu, CardReset]:
"""Main function to call by the user: Blocking read, returns Apdu or CardReset."""
@@ -31,5 +30,5 @@ class ApduSource(abc.ABC):
elif isinstance(r, CardReset):
apdu = r
else:
- ValueError('Unknown read_packet() return %s' % r)
+ raise ValueError('Unknown read_packet() return %s' % r)
return apdu
diff --git a/pySim/apdu_source/gsmtap.py b/pySim/apdu_source/gsmtap.py
index fe450e2..bcfb66d 100644
--- a/pySim/apdu_source/gsmtap.py
+++ b/pySim/apdu_source/gsmtap.py
@@ -16,12 +16,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from pySim.gsmtap import GsmtapMessage, GsmtapSource
-from . import ApduSource, PacketType, CardReset
+from pySim.gsmtap import GsmtapSource
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
+
+from . import ApduSource, PacketType, CardReset
+
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
class GsmtapApduSource(ApduSource):
@@ -41,16 +43,16 @@ class GsmtapApduSource(ApduSource):
self.gsmtap = GsmtapSource(bind_ip, bind_port)
def read_packet(self) -> PacketType:
- gsmtap_msg, addr = self.gsmtap.read_packet()
+ gsmtap_msg, _addr = self.gsmtap.read_packet()
if gsmtap_msg['type'] != 'sim':
raise ValueError('Unsupported GSMTAP type %s' % gsmtap_msg['type'])
sub_type = gsmtap_msg['sub_type']
if sub_type == 'apdu':
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
- elif sub_type == 'atr':
+ if sub_type == 'atr':
# card has been reset
- return CardReset()
- elif sub_type in ['pps_req', 'pps_rsp']:
+ return CardReset(gsmtap_msg['body'])
+ if sub_type in ['pps_req', 'pps_rsp']:
# simply ignore for now
pass
else:
diff --git a/pySim/apdu_source/pyshark_gsmtap.py b/pySim/apdu_source/pyshark_gsmtap.py
index 8ea9ae7..83745e9 100644
--- a/pySim/apdu_source/pyshark_gsmtap.py
+++ b/pySim/apdu_source/pyshark_gsmtap.py
@@ -16,20 +16,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import sys
import logging
-from pprint import pprint as pp
from typing import Tuple
import pyshark
-from pySim.utils import h2b, b2h
-from pySim.apdu import Tpdu
+from pySim.utils import h2b
from pySim.gsmtap import GsmtapMessage
-from . import ApduSource, PacketType, CardReset
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
+
+from . import ApduSource, PacketType, CardReset
+
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
logger = logging.getLogger(__name__)
@@ -67,10 +66,10 @@ class _PysharkGsmtap(ApduSource):
sub_type = gsmtap_msg['sub_type']
if sub_type == 'apdu':
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
- elif sub_type == 'atr':
+ if sub_type == 'atr':
# card has been reset
- return CardReset()
- elif sub_type in ['pps_req', 'pps_rsp']:
+ return CardReset(gsmtap_msg['body'])
+ if sub_type in ['pps_req', 'pps_rsp']:
# simply ignore for now
pass
else:
@@ -87,4 +86,3 @@ class PysharkGsmtapPcap(_PysharkGsmtap):
"""
pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='gsm_sim', use_json=True, keep_packets=False)
super().__init__(pyshark_inst)
-
diff --git a/pySim/apdu_source/pyshark_rspro.py b/pySim/apdu_source/pyshark_rspro.py
index 499e9ff..a0e30ed 100644
--- a/pySim/apdu_source/pyshark_rspro.py
+++ b/pySim/apdu_source/pyshark_rspro.py
@@ -16,13 +16,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import sys
import logging
-from pprint import pprint as pp
from typing import Tuple
import pyshark
-from pySim.utils import h2b, b2h
+from pySim.utils import h2b
from pySim.apdu import Tpdu
from . import ApduSource, PacketType, CardReset
@@ -89,7 +87,7 @@ class _PysharkRspro(ApduSource):
bsl = self.get_bank_slot(bank_slot)
self._set_or_verify_bank_slot(bsl)
data = modem2card.get_field('data').replace(':','')
- logger.debug("C(%u:%u) -> B(%u:%u): %s" % (csl[0], csl[1], bsl[0], bsl[1], data))
+ logger.debug("C(%u:%u) -> B(%u:%u): %s", csl[0], csl[1], bsl[0], bsl[1], data)
# store the CMD portion until the RSP portion arrives later
self.cmd_tpdu = h2b(data)
elif msg_type == '13': # tpduCardToModem
@@ -101,7 +99,7 @@ class _PysharkRspro(ApduSource):
bsl = self.get_bank_slot(bank_slot)
self._set_or_verify_bank_slot(bsl)
data = card2modem.get_field('data').replace(':','')
- logger.debug("C(%u:%u) <- B(%u:%u): %s" % (csl[0], csl[1], bsl[0], bsl[1], data))
+ logger.debug("C(%u:%u) <- B(%u:%u): %s", csl[0], csl[1], bsl[0], bsl[1], data)
rsp_tpdu = h2b(data)
if self.cmd_tpdu:
# combine this R-TPDU with the C-TPDU we saw earlier
@@ -117,7 +115,8 @@ class _PysharkRspro(ApduSource):
vccPresent, resetActive, clkActive = self.get_pstatus(slot_pstatus)
if vccPresent and clkActive and not resetActive:
logger.debug("RESET")
- return CardReset()
+ #TODO: extract ATR from RSPRO message and use it here
+ return CardReset(None)
else:
print("Unhandled msg type %s: %s" % (msg_type, rspro_msg))
diff --git a/pySim/apdu_source/tca_loader_log.py b/pySim/apdu_source/tca_loader_log.py
new file mode 100644
index 0000000..6efc605
--- /dev/null
+++ b/pySim/apdu_source/tca_loader_log.py
@@ -0,0 +1,48 @@
+# coding=utf-8
+
+# (C) 2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from pySim.utils import h2b
+from pySim.gsmtap import GsmtapSource
+
+from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
+from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
+from pySim.apdu.global_platform import ApduCommands as GpApduCommands
+
+from . import ApduSource, PacketType, CardReset
+
+ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
+
+class TcaLoaderLogApduSource(ApduSource):
+ """ApduSource for reading log files created by TCALoader."""
+ def __init__(self, filename:str):
+ super().__init__()
+ self.logfile = open(filename, 'r')
+
+ def read_packet(self) -> PacketType:
+ command = None
+ response = None
+ for line in self.logfile:
+ if line.startswith('Command'):
+ command = line.split()[1]
+ print("Command: '%s'" % command)
+ pass
+ elif command and line.startswith('Response'):
+ response = line.split()[1]
+ print("Response: '%s'" % response)
+ return ApduCommands.parse_cmd_bytes(h2b(command) + h2b(response))
+ raise StopIteration
diff --git a/pySim/app.py b/pySim/app.py
new file mode 100644
index 0000000..22f5b92
--- /dev/null
+++ b/pySim/app.py
@@ -0,0 +1,122 @@
+# (C) 2021-2023 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from typing import Tuple
+
+from pySim.transport import LinkBase
+from pySim.commands import SimCardCommands
+from pySim.filesystem import CardModel, CardApplication
+from pySim.cards import card_detect, SimCardBase, UiccCardBase
+from pySim.runtime import RuntimeState
+from pySim.profile import CardProfile
+from pySim.cdma_ruim import CardProfileRUIM
+from pySim.ts_102_221 import CardProfileUICC
+from pySim.utils import all_subclasses
+from pySim.exceptions import SwMatchError
+
+# we need to import this module so that the SysmocomSJA2 sub-class of
+# CardModel is created, which will add the ATR-based matching and
+# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
+import pySim.sysmocom_sja2
+#import pySim.sysmocom_euicc1
+
+# we need to import these modules so that the various sub-classes of
+# CardProfile are created, which will be used in init_card() to iterate
+# over all known CardProfile sub-classes.
+import pySim.ts_31_102
+import pySim.ts_31_103
+import pySim.ts_31_104
+import pySim.ara_m
+import pySim.global_platform
+import pySim.euicc
+
+def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
+ """
+ Detect card in reader and setup card profile and runtime state. This
+ function must be called at least once on startup. The card and runtime
+ state object (rs) is required for all pySim-shell commands.
+ """
+
+ # Create command layer
+ scc = SimCardCommands(transport=sl)
+
+ # Wait up to three seconds for a card in reader and try to detect
+ # the card type.
+ print("Waiting for card...")
+ sl.wait_for_card(3)
+
+ generic_card = False
+ card = card_detect(scc)
+ if card is None:
+ print("Warning: Could not detect card type - assuming a generic card type...")
+ card = SimCardBase(scc)
+ generic_card = True
+
+ profile = CardProfile.pick(scc)
+ if profile is None:
+ # It is not an unrecoverable error in case profile detection fails. It
+ # just means that pySim was unable to recognize the card profile. This
+ # may happen in particular with unprovisioned cards that do not have
+ # any files on them yet.
+ print("Unsupported card type!")
+ return None, card
+
+ # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
+ # references, however card manufactures may still decide to pick an
+ # arbitrary key reference. In case we run on a generic card class that is
+ # detected as an UICC, we will pick the key reference that is officially
+ # specified.
+ if generic_card and isinstance(profile, CardProfileUICC):
+ card._adm_chv_num = 0x0A
+
+ print("Info: Card is of type: %s" % str(profile))
+
+ # FIXME: this shouldn't really be here but somewhere else/more generic.
+ # We cannot do it within pySim/profile.py as that would create circular
+ # dependencies between the individual profiles and profile.py.
+ if isinstance(profile, CardProfileUICC):
+ for app_cls in all_subclasses(CardApplication):
+ # skip any intermediary sub-classes such as CardApplicationSD
+ if hasattr(app_cls, '_' + app_cls.__name__ + '__intermediate'):
+ continue
+ profile.add_application(app_cls())
+ # We have chosen SimCard() above, but we now know it actually is an UICC
+ # so it's safe to assume it supports USIM application (which we're adding above).
+ # IF we don't do this, we will have a SimCard but try USIM specific commands like
+ # the update_ust method (see https://osmocom.org/issues/6055)
+ if generic_card:
+ card = UiccCardBase(scc)
+
+ # Create runtime state with card profile
+ rs = RuntimeState(card, profile)
+
+ CardModel.apply_matching_models(scc, rs)
+
+ # inform the transport that we can do context-specific SW interpretation
+ sl.set_sw_interpreter(rs)
+
+ # try to obtain the EID, if any
+ isd_r = rs.mf.applications.get(pySim.euicc.AID_ISD_R.lower(), None)
+ if isd_r:
+ rs.lchan[0].select_file(isd_r)
+ try:
+ rs.identity['EID'] = pySim.euicc.CardApplicationISDR.get_eid(scc)
+ except SwMatchError:
+ # has ISD-R but not a SGP.22/SGP.32 eUICC - maybe SGP.02?
+ pass
+ card.reset()
+
+ return rs, card
diff --git a/pySim/ara_m.py b/pySim/ara_m.py
index 59f205c..bd55d35 100644
--- a/pySim/ara_m.py
+++ b/pySim/ara_m.py
@@ -26,11 +26,13 @@ Support for the Secure Element Access Control, specifically the ARA-M inside an
#
-from construct import *
+from construct import GreedyBytes, GreedyString, Struct, Enum, Int8ub, Int16ub
from construct import Optional as COptional
from pySim.construct import *
from pySim.filesystem import *
from pySim.tlv import *
+from pySim.utils import Hexstr
+import pySim.global_platform
# various BER-TLV encoded Data Objects (DOs)
@@ -67,11 +69,10 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
if do[0] == 0x00:
self.decoded = {'generic_access_rule': 'never'}
return self.decoded
- elif do[0] == 0x01:
+ if do[0] == 0x01:
self.decoded = {'generic_access_rule': 'always'}
return self.decoded
- else:
- return ValueError('Invalid 1-byte generic APDU access rule')
+ return ValueError('Invalid 1-byte generic APDU access rule')
else:
if len(do) % 8:
return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
@@ -87,10 +88,9 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
if 'generic_access_rule' in self.decoded:
if self.decoded['generic_access_rule'] == 'never':
return b'\x00'
- elif self.decoded['generic_access_rule'] == 'always':
+ if self.decoded['generic_access_rule'] == 'always':
return b'\x01'
- else:
- return ValueError('Invalid 1-byte generic APDU access rule')
+ return ValueError('Invalid 1-byte generic APDU access rule')
else:
if not 'apdu_filter' in self.decoded:
return ValueError('Invalid APDU AR DO')
@@ -115,6 +115,7 @@ class NfcArDO(BER_TLV_IE, tag=0xd1):
class PermArDO(BER_TLV_IE, tag=0xdb):
# Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
+ # based on Table 6-8 of GlobalPlatform Device API Access Control v1.0
_construct = Struct('permissions'/HexAdapter(Bytes(8)))
@@ -259,6 +260,9 @@ class ADF_ARAM(CardADF):
files = []
self.add_files(files)
+ def decode_select_response(self, data_hex):
+ return pySim.global_platform.decode_select_response(data_hex)
+
@staticmethod
def xceive_apdu_tlv(tp, hdr: Hexstr, cmd_do, resp_cls, exp_sw='9000'):
"""Transceive an APDU with the card, transparently encoding the command data from TLV
@@ -272,14 +276,13 @@ class ADF_ARAM(CardADF):
cmd_do_enc = b''
cmd_do_len = 0
c_apdu = hdr + ('%02x' % cmd_do_len) + b2h(cmd_do_enc)
- (data, sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
+ (data, _sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
if data:
if resp_cls:
resp_do = resp_cls()
resp_do.from_tlv(h2b(data))
return resp_do
- else:
- return data
+ return data
else:
return None
@@ -295,24 +298,21 @@ class ADF_ARAM(CardADF):
@staticmethod
def get_config(tp, v_major=0, v_minor=0, v_patch=1):
cmd_do = DeviceConfigDO()
- cmd_do.from_dict([{'DeviceInterfaceVersionDO': {
+ cmd_do.from_dict([{'device_interface_version_do': {
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
return ADF_ARAM.xceive_apdu_tlv(tp, '80cadf21', cmd_do, ResponseAramConfigDO)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
- def __init(self):
- super().__init__()
-
- def do_aram_get_all(self, opts):
+ def do_aram_get_all(self, _opts):
"""GET DATA [All] on the ARA-M Applet"""
- res_do = ADF_ARAM.get_all(self._cmd.card._scc._tp)
+ res_do = ADF_ARAM.get_all(self._cmd.lchan.scc._tp)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
- def do_aram_get_config(self, opts):
+ def do_aram_get_config(self, _opts):
"""Perform GET DATA [Config] on the ARA-M Applet: Tell it our version and retrieve its version."""
- res_do = ADF_ARAM.get_config(self._cmd.card._scc._tp)
+ res_do = ADF_ARAM.get_config(self._cmd.lchan.scc._tp)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
@@ -348,39 +348,39 @@ class ADF_ARAM(CardADF):
"""Perform STORE DATA [Command-Store-REF-AR-DO] to store a (new) access rule."""
# REF
ref_do_content = []
- if opts.aid:
- ref_do_content += [{'AidRefDO': opts.aid}]
+ if opts.aid is not None:
+ ref_do_content += [{'aid_ref_do': opts.aid}]
elif opts.aid_empty:
- ref_do_content += [{'AidRefEmptyDO': None}]
- ref_do_content += [{'DevAppIdRefDO': opts.device_app_id}]
+ ref_do_content += [{'aid_ref_empty_do': None}]
+ ref_do_content += [{'dev_app_id_ref_do': opts.device_app_id}]
if opts.pkg_ref:
- ref_do_content += [{'PkgRefDO': opts.pkg_ref}]
+ ref_do_content += [{'pkg_ref_do': {'package_name_string': opts.pkg_ref}}]
# AR
ar_do_content = []
if opts.apdu_never:
- ar_do_content += [{'ApduArDO': {'generic_access_rule': 'never'}}]
+ ar_do_content += [{'apdu_ar_od': {'generic_access_rule': 'never'}}]
elif opts.apdu_always:
- ar_do_content += [{'ApduArDO': {'generic_access_rule': 'always'}}]
+ ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
elif opts.apdu_filter:
# TODO: multiple filters
- ar_do_content += [{'ApduArDO': {'apdu_filter': [opts.apdu_filter]}}]
+ ar_do_content += [{'apdu_ar_do': {'apdu_filter': [opts.apdu_filter]}}]
if opts.nfc_always:
- ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'always'}}]
+ ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
elif opts.nfc_never:
- ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'never'}}]
+ ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'never'}}]
if opts.android_permissions:
- ar_do_content += [{'PermArDO': {'permissions': opts.android_permissions}}]
- d = [{'RefArDO': [{'RefDO': ref_do_content}, {'ArDO': ar_do_content}]}]
+ ar_do_content += [{'perm_ar_do': {'permissions': opts.android_permissions}}]
+ d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
csrado = CommandStoreRefArDO()
csrado.from_dict(d)
- res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, csrado)
+ res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, csrado)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
- def do_aram_delete_all(self, opts):
+ def do_aram_delete_all(self, _opts):
"""Perform STORE DATA [Command-Delete[all]] to delete all access rules."""
deldo = CommandDelete()
- res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, deldo)
+ res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, deldo)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
@@ -410,3 +410,9 @@ sw_aram = {
class CardApplicationARAM(CardApplication):
def __init__(self):
super().__init__('ARA-M', adf=ADF_ARAM(), sw=sw_aram)
+
+ def export(self, cmd, context, **kwargs):
+ cmd.lchan.select_file(self.adf)
+ res_do = ADF_ARAM.get_all(cmd.card._scc._tp)
+ if res_do:
+ cmd.poutput_json(res_do.to_dict())
diff --git a/pySim/card_handler.py b/pySim/card_handler.py
index 57e0d32..e7884ad 100644
--- a/pySim/card_handler.py
+++ b/pySim/card_handler.py
@@ -24,12 +24,11 @@ there are also automatic card feeders.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-from pySim.transport import LinkBase
-
import subprocess
import sys
import yaml
+from pySim.transport import LinkBase
class CardHandlerBase:
"""Abstract base class representing a mechanism for card insertion/removal."""
@@ -97,7 +96,7 @@ class CardHandlerAuto(CardHandlerBase):
print("Card handler Config-file: " + str(config_file))
with open(config_file) as cfg:
self.cmds = yaml.load(cfg, Loader=yaml.FullLoader)
- self.verbose = (self.cmds.get('verbose') == True)
+ self.verbose = self.cmds.get('verbose') is True
def __print_outout(self, out):
print("")
diff --git a/pySim/card_key_provider.py b/pySim/card_key_provider.py
index 00e32aa..6751b09 100644
--- a/pySim/card_key_provider.py
+++ b/pySim/card_key_provider.py
@@ -10,10 +10,10 @@ the need of manually entering the related card-individual data on every
operation with pySim-shell.
"""
-# (C) 2021 by Sysmocom s.f.m.c. GmbH
+# (C) 2021-2024 by Sysmocom s.f.m.c. GmbH
# All Rights Reserved
#
-# Author: Philipp Maier
+# Author: Philipp Maier, Harald Welte
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -29,18 +29,29 @@ operation with pySim-shell.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List, Dict, Optional
+from Cryptodome.Cipher import AES
+from pySim.utils import h2b, b2h
import abc
import csv
card_key_providers = [] # type: List['CardKeyProvider']
+# well-known groups of columns relate to a given functionality. This avoids having
+# to specify the same transport key N number of times, if the same key is used for multiple
+# fields of one group, like KIC+KID+KID of one SD.
+CRYPT_GROUPS = {
+ 'UICC_SCP02': ['UICC_SCP02_KIC1', 'UICC_SCP02_KID1', 'UICC_SCP02_KIK1'],
+ 'UICC_SCP03': ['UICC_SCP03_KIC1', 'UICC_SCP03_KID1', 'UICC_SCP03_KIK1'],
+ 'SCP03_ISDR': ['SCP03_ENC_ISDR', 'SCP03_MAC_ISDR', 'SCP03_DEK_ISDR'],
+ 'SCP03_ISDA': ['SCP03_ENC_ISDR', 'SCP03_MAC_ISDA', 'SCP03_DEK_ISDA'],
+ 'SCP03_ECASD': ['SCP03_ENC_ECASD', 'SCP03_MAC_ECASD', 'SCP03_DEK_ECASD'],
+ }
class CardKeyProvider(abc.ABC):
"""Base class, not containing any concrete implementation."""
- VALID_FIELD_NAMES = ['ICCID', 'ADM1',
- 'IMSI', 'PIN1', 'PIN2', 'PUK1', 'PUK2']
+ VALID_KEY_FIELD_NAMES = ['ICCID', 'EID', 'IMSI' ]
# check input parameters, but do nothing concrete yet
def _verify_get_data(self, fields: List[str] = [], key: str = 'ICCID', value: str = "") -> Dict[str, str]:
@@ -53,14 +64,10 @@ class CardKeyProvider(abc.ABC):
Returns:
dictionary of {field, value} strings for each requested field from 'fields'
"""
- for f in fields:
- if (f not in self.VALID_FIELD_NAMES):
- raise ValueError("Requested field name '%s' is not a valid field name, valid field names are: %s" %
- (f, str(self.VALID_FIELD_NAMES)))
- if (key not in self.VALID_FIELD_NAMES):
+ if key not in self.VALID_KEY_FIELD_NAMES:
raise ValueError("Key field name '%s' is not a valid field name, valid field names are: %s" %
- (key, str(self.VALID_FIELD_NAMES)))
+ (key, str(self.VALID_KEY_FIELD_NAMES)))
return {}
@@ -84,19 +91,47 @@ class CardKeyProvider(abc.ABC):
class CardKeyProviderCsv(CardKeyProvider):
- """Card key provider implementation that allows to query against a specified CSV file"""
+ """Card key provider implementation that allows to query against a specified CSV file.
+ Supports column-based encryption as it is generally a bad idea to store cryptographic key material in
+ plaintext. Instead, the key material should be encrypted by a "key-encryption key", occasionally also
+ known as "transport key" (see GSMA FS.28)."""
+ IV = b'\x23' * 16
csv_file = None
filename = None
- def __init__(self, filename: str):
+ def __init__(self, filename: str, transport_keys: dict):
"""
Args:
filename : file name (path) of CSV file containing card-individual key/data
+ transport_keys : a dict indexed by field name, whose values are hex-encoded AES keys for the
+ respective field (column) of the CSV. This is done so that different fields
+ (columns) can use different transport keys, which is strongly recommended by
+ GSMA FS.28
"""
self.csv_file = open(filename, 'r')
if not self.csv_file:
raise RuntimeError("Could not open CSV file '%s'" % filename)
self.filename = filename
+ self.transport_keys = self.process_transport_keys(transport_keys)
+
+ @staticmethod
+ def process_transport_keys(transport_keys: dict):
+ """Apply a single transport key to multiple fields/columns, if the name is a group."""
+ new_dict = {}
+ for name, key in transport_keys.items():
+ if name in CRYPT_GROUPS:
+ for field in CRYPT_GROUPS[name]:
+ new_dict[field] = key
+ else:
+ new_dict[name] = key
+ return new_dict
+
+ def _decrypt_field(self, field_name: str, encrypted_val: str) -> str:
+ """decrypt a single field, if we have a transport key for the field of that name."""
+ if not field_name in self.transport_keys:
+ return encrypted_val
+ cipher = AES.new(h2b(self.transport_keys[field_name]), AES.MODE_CBC, self.IV)
+ return b2h(cipher.decrypt(h2b(encrypted_val)))
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
super()._verify_get_data(fields, key, value)
@@ -113,7 +148,7 @@ class CardKeyProviderCsv(CardKeyProvider):
if row[key] == value:
for f in fields:
if f in row:
- rc.update({f: row[f]})
+ rc.update({f: self._decrypt_field(f, row[f])})
else:
raise RuntimeError("CSV-File '%s' lacks column '%s'" %
(self.filename, f))
diff --git a/pySim/cards.py b/pySim/cards.py
index d3a43f3..802c921 100644
--- a/pySim/cards.py
+++ b/pySim/cards.py
@@ -5,7 +5,7 @@
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
-# Copyright (C) 2011 Harald Welte <laforge@gnumonks.org>
+# Copyright (C) 2011-2023 Harald Welte <laforge@gnumonks.org>
# Copyright (C) 2017 Alexander.Chemeris <Alexander.Chemeris@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
@@ -22,267 +22,90 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-from typing import Optional, Dict, Tuple
-import abc
+from typing import Optional, Tuple
+from pySim.ts_102_221 import EF_DIR
+from pySim.ts_51_011 import DF_GSM
-from pySim.ts_51_011 import EF, DF, EF_AD, EF_SPN
-from pySim.ts_31_102 import EF_USIM_ADF_map
-from pySim.ts_31_103 import EF_ISIM_ADF_map
from pySim.utils import *
-from smartcard.util import toBytes
-from pytlv.TLV import *
+from pySim.commands import Path, SimCardCommands
-
-def format_addr(addr: str, addr_type: str) -> str:
- """
- helper function to format an FQDN (addr_type = '00') or IPv4
- (addr_type = '01') address string into a printable string that
- contains the hexadecimal representation and the original address
- string (addr)
- """
- res = ""
- if addr_type == '00': # FQDN
- res += "\t%s # %s\n" % (s2h(addr), addr)
- elif addr_type == '01': # IPv4
- octets = addr.split(".")
- addr_hex = ""
- for o in octets:
- addr_hex += ("%02x" % int(o))
- res += "\t%s # %s\n" % (addr_hex, addr)
- return res
-
-
-class SimCard:
-
- name = 'SIM'
-
- def __init__(self, scc):
+class CardBase:
+ """General base class for some kind of telecommunications card."""
+ def __init__(self, scc: SimCardCommands):
self._scc = scc
- self._adm_chv_num = 4
self._aids = []
- def reset(self):
+ def reset(self) -> Optional[Hexstr]:
rc = self._scc.reset_card()
if rc == 1:
return self._scc.get_atr()
- else:
- return None
+ return None
+
+ def set_apdu_parameter(self, cla: Hexstr, sel_ctrl: Hexstr) -> None:
+ """Set apdu parameters (class byte and selection control bytes)"""
+ self._scc.cla_byte = cla
+ self._scc.sel_ctrl = sel_ctrl
+
+ def get_apdu_parameter(self) -> Tuple[Hexstr, Hexstr]:
+ """Get apdu parameters (class byte and selection control bytes)"""
+ return (self._scc.cla_byte, self._scc.sel_ctrl)
def erase(self):
print("warning: erasing is not supported for specified card type!")
- return
- def file_exists(self, fid):
+ def file_exists(self, fid: Path) -> bool:
res_arr = self._scc.try_select_path(fid)
for res in res_arr:
if res[1] != '9000':
return False
return True
- def verify_adm(self, key):
- """Authenticate with ADM key"""
- (res, sw) = self._scc.verify_chv(self._adm_chv_num, key)
- return sw
-
- def read_iccid(self):
- (res, sw) = self._scc.read_binary(EF['ICCID'])
- if sw == '9000':
- return (dec_iccid(res), sw)
- else:
- return (None, sw)
-
- def update_iccid(self, iccid):
- data, sw = self._scc.update_binary(EF['ICCID'], enc_iccid(iccid))
- return sw
-
- def read_imsi(self):
- (res, sw) = self._scc.read_binary(EF['IMSI'])
- if sw == '9000':
- return (dec_imsi(res), sw)
- else:
- return (None, sw)
-
- def update_imsi(self, imsi):
- data, sw = self._scc.update_binary(EF['IMSI'], enc_imsi(imsi))
- return sw
-
- def update_acc(self, acc):
- data, sw = self._scc.update_binary(EF['ACC'], lpad(acc, 4, c='0'))
- return sw
-
- def read_hplmn_act(self):
- (res, sw) = self._scc.read_binary(EF['HPLMNAcT'])
- if sw == '9000':
- return (format_xplmn_w_act(res), sw)
- else:
- return (None, sw)
+ def read_aids(self) -> List[Hexstr]:
+ # a non-UICC doesn't have any applications. Convenience helper to avoid
+ # callers having to do hasattr('read_aids') ahead of every call.
+ return []
- def update_hplmn_act(self, mcc, mnc, access_tech='FFFF'):
- """
- Update Home PLMN with access technology bit-field
- See Section "10.3.37 EFHPLMNwAcT (HPLMN Selector with Access Technology)"
- in ETSI TS 151 011 for the details of the access_tech field coding.
- Some common values:
- access_tech = '0080' # Only GSM is selected
- access_tech = 'FFFF' # All technologies selected, even Reserved for Future Use ones
- """
- # get size and write EF.HPLMNwAcT
- data = self._scc.read_binary(EF['HPLMNwAcT'], length=None, offset=0)
- size = len(data[0]) // 2
- hplmn = enc_plmn(mcc, mnc)
- content = hplmn + access_tech
- data, sw = self._scc.update_binary(
- EF['HPLMNwAcT'], content + 'ffffff0000' * (size // 5 - 1))
- return sw
-
- def read_oplmn_act(self):
- (res, sw) = self._scc.read_binary(EF['OPLMNwAcT'])
- if sw == '9000':
- return (format_xplmn_w_act(res), sw)
- else:
- return (None, sw)
-
- def update_oplmn_act(self, mcc, mnc, access_tech='FFFF'):
- """get size and write EF.OPLMNwAcT, See note in update_hplmn_act()"""
- data = self._scc.read_binary(EF['OPLMNwAcT'], length=None, offset=0)
- size = len(data[0]) // 2
- hplmn = enc_plmn(mcc, mnc)
- content = hplmn + access_tech
- data, sw = self._scc.update_binary(
- EF['OPLMNwAcT'], content + 'ffffff0000' * (size // 5 - 1))
- return sw
-
- def read_plmn_act(self):
- (res, sw) = self._scc.read_binary(EF['PLMNwAcT'])
- if sw == '9000':
- return (format_xplmn_w_act(res), sw)
- else:
- return (None, sw)
-
- def update_plmn_act(self, mcc, mnc, access_tech='FFFF'):
- """get size and write EF.PLMNwAcT, See note in update_hplmn_act()"""
- data = self._scc.read_binary(EF['PLMNwAcT'], length=None, offset=0)
- size = len(data[0]) // 2
- hplmn = enc_plmn(mcc, mnc)
- content = hplmn + access_tech
- data, sw = self._scc.update_binary(
- EF['PLMNwAcT'], content + 'ffffff0000' * (size // 5 - 1))
- return sw
-
- def update_plmnsel(self, mcc, mnc):
- data = self._scc.read_binary(EF['PLMNsel'], length=None, offset=0)
- size = len(data[0]) // 2
- hplmn = enc_plmn(mcc, mnc)
- data, sw = self._scc.update_binary(
- EF['PLMNsel'], hplmn + 'ff' * (size-3))
- return sw
-
- def update_smsp(self, smsp):
- data, sw = self._scc.update_record(EF['SMSP'], 1, rpad(smsp, 84))
- return sw
-
- def update_ad(self, mnc=None, opmode=None, ofm=None, path=EF['AD']):
- """
- Update Administrative Data (AD)
-
- See Sec. "4.2.18 EF_AD (Administrative Data)"
- in 3GPP TS 31.102 for the details of the EF_AD contents.
-
- Set any parameter to None to keep old value(s) on card.
-
- Parameters:
- mnc (str): MNC of IMSI
- opmode (Hex-str, 1 Byte): MS Operation Mode
- ofm (Hex-str, 1 Byte): Operational Feature Monitor (OFM) aka Ciphering Indicator
- path (optional list with file path e.g. ['3f00', '7f20', '6fad'])
-
- Returns:
- str: Return code of write operation
- """
-
- ad = EF_AD()
-
- # read from card
- raw_hex_data, sw = self._scc.read_binary(
- path, length=None, offset=0)
- abstract_data = ad.decode_hex(raw_hex_data)
-
- # perform updates
- if mnc and abstract_data['extensions']:
- # Note: Since we derive the length of the MNC by the string length
- # of the mnc parameter, the caller must ensure that mnc has the
- # correct length and is padded with zeros (if necessary).
- mnclen = len(str(mnc))
- if mnclen > 3 or mnclen < 2:
- raise RuntimeError('invalid length of mnc "{}", expecting 2 or 3 digits'.format(mnc))
- abstract_data['extensions']['mnc_len'] = mnclen
- if opmode:
- opmode_num = int(opmode, 16)
- if opmode_num in [int(v) for v in EF_AD.OP_MODE]:
- abstract_data['ms_operation_mode'] = opmode_num
- else:
- raise RuntimeError('invalid opmode "{}"'.format(opmode))
- if ofm:
- abstract_data['ofm'] = bool(int(ofm, 16))
-
- # write to card
- raw_hex_data = ad.encode_hex(abstract_data)
- data, sw = self._scc.update_binary(path, raw_hex_data)
- return sw
+class SimCardBase(CardBase):
+ """Here we only add methods for commands specified in TS 51.011, without
+ any higher-layer processing."""
+ name = 'SIM'
- def read_spn(self):
- (content, sw) = self._scc.read_binary(EF['SPN'])
- if sw == '9000':
- abstract_data = EF_SPN().decode_hex(content)
- show_in_hplmn = abstract_data['show_in_hplmn']
- hide_in_oplmn = abstract_data['hide_in_oplmn']
- name = abstract_data['spn']
- return ((name, show_in_hplmn, hide_in_oplmn), sw)
- else:
- return (None, sw)
+ def __init__(self, scc: SimCardCommands):
+ super().__init__(scc)
+ self._scc.cla_byte = "A0"
+ self._scc.sel_ctrl = "0000"
- def update_spn(self, name="", show_in_hplmn=False, hide_in_oplmn=False):
- abstract_data = {
- 'hide_in_oplmn': hide_in_oplmn,
- 'show_in_hplmn': show_in_hplmn,
- 'spn': name,
- }
- content = EF_SPN().encode_hex(abstract_data)
- data, sw = self._scc.update_binary(EF['SPN'], content)
- return sw
+ def probe(self) -> bool:
+ df_gsm = DF_GSM()
+ return self.file_exists(df_gsm.fid)
- def read_binary(self, ef, length=None, offset=0):
- ef_path = ef in EF and EF[ef] or ef
- return self._scc.read_binary(ef_path, length, offset)
- def read_record(self, ef, rec_no):
- ef_path = ef in EF and EF[ef] or ef
- return self._scc.read_record(ef_path, rec_no)
+class UiccCardBase(SimCardBase):
+ name = 'UICC'
- def read_gid1(self):
- (res, sw) = self._scc.read_binary(EF['GID1'])
- if sw == '9000':
- return (res, sw)
- else:
- return (None, sw)
+ def __init__(self, scc: SimCardCommands):
+ super().__init__(scc)
+ self._scc.cla_byte = "00"
+ self._scc.sel_ctrl = "0004" # request an FCP
+ # See also: ETSI TS 102 221, Table 9.3
+ self._adm_chv_num = 0x0A
- def read_msisdn(self):
- (res, sw) = self._scc.read_record(EF['MSISDN'], 1)
- if sw == '9000':
- return (dec_msisdn(res), sw)
- else:
- return (None, sw)
+ def probe(self) -> bool:
+ # EF.DIR is a mandatory EF on all ICCIDs; however it *may* also exist on a TS 51.011 SIM
+ ef_dir = EF_DIR()
+ return self.file_exists(ef_dir.fid)
- def read_aids(self):
+ def read_aids(self) -> List[Hexstr]:
"""Fetch all the AIDs present on UICC"""
self._aids = []
try:
+ ef_dir = EF_DIR()
# Find out how many records the EF.DIR has
# and store all the AIDs in the UICC
- rec_cnt = self._scc.record_count(EF['DIR'])
+ rec_cnt = self._scc.record_count(ef_dir.fid)
for i in range(0, rec_cnt):
- rec = self._scc.read_record(EF['DIR'], i + 1)
+ rec = self._scc.read_record(ef_dir.fid, i + 1)
if (rec[0][0:2], rec[0][4:6]) == ('61', '4f') and len(rec[0]) > 12 \
and rec[0][8:8 + int(rec[0][6:8], 16) * 2] not in self._aids:
self._aids.append(rec[0][8:8 + int(rec[0][6:8], 16) * 2])
@@ -292,7 +115,7 @@ class SimCard:
return self._aids
@staticmethod
- def _get_aid(adf="usim") -> str:
+ def _get_aid(adf="usim") -> Optional[Hexstr]:
aid_map = {}
# First (known) halves of the U/ISIM AID
aid_map["usim"] = "a0000000871002"
@@ -302,7 +125,7 @@ class SimCard:
return aid_map[adf]
return None
- def _complete_aid(self, aid) -> str:
+ def _complete_aid(self, aid: Hexstr) -> Optional[Hexstr]:
"""find the complete version of an ADF.U/ISIM AID"""
# Find full AID by partial AID:
if is_hex(aid):
@@ -311,7 +134,7 @@ class SimCard:
return aid_known
return None
- def adf_present(self, adf="usim") -> bool:
+ def adf_present(self, adf: str = "usim") -> bool:
"""Check if the AID of the specified ADF is present in EF.DIR (call read_aids before use)"""
aid = self._get_aid(adf)
if aid:
@@ -320,8 +143,10 @@ class SimCard:
return True
return False
- def select_adf_by_aid(self, adf="usim"):
+ def select_adf_by_aid(self, adf: str = "usim", scc: Optional[SimCardCommands] = None) -> Tuple[Optional[Hexstr], Optional[SwHexstr]]:
"""Select ADF.U/ISIM in the Card using its full AID"""
+ # caller may pass a custom scc; we fall back to default
+ scc = scc or self._scc
if is_hex(adf):
aid = adf
else:
@@ -329,1469 +154,20 @@ class SimCard:
if aid:
aid_full = self._complete_aid(aid)
if aid_full:
- return self._scc.select_adf(aid_full)
- else:
- # If we cannot get the full AID, try with short AID
- return self._scc.select_adf(aid)
+ return scc.select_adf(aid_full)
+ # If we cannot get the full AID, try with short AID
+ return scc.select_adf(aid)
return (None, None)
- def erase_binary(self, ef):
- """Erase the contents of a file"""
- len = self._scc.binary_size(ef)
- self._scc.update_binary(ef, "ff" * len, offset=0, verify=True)
-
- def erase_record(self, ef, rec_no):
- """Erase the contents of a single record"""
- len = self._scc.record_size(ef)
- self._scc.update_record(ef, rec_no, "ff" * len,
- force_len=False, verify=True)
-
- def set_apdu_parameter(self, cla, sel_ctrl):
- """Set apdu parameters (class byte and selection control bytes)"""
- self._scc.cla_byte = cla
- self._scc.sel_ctrl = sel_ctrl
-
- def get_apdu_parameter(self):
- """Get apdu parameters (class byte and selection control bytes)"""
- return (self._scc.cla_byte, self._scc.sel_ctrl)
-
-
-class UsimCard(SimCard):
-
- name = 'USIM'
-
- def __init__(self, ssc):
- super(UsimCard, self).__init__(ssc)
-
- # See also: ETSI TS 102 221, Table 9.3
- self._adm_chv_num = 0xA0
-
- def read_ehplmn(self):
- (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['EHPLMN'])
- if sw == '9000':
- return (format_xplmn(res), sw)
- else:
- return (None, sw)
-
- def update_ehplmn(self, mcc, mnc):
- data = self._scc.read_binary(
- EF_USIM_ADF_map['EHPLMN'], length=None, offset=0)
- size = len(data[0]) // 2
- ehplmn = enc_plmn(mcc, mnc)
- data, sw = self._scc.update_binary(EF_USIM_ADF_map['EHPLMN'], ehplmn)
- return sw
-
- def read_fplmn(self):
- res, sw = self._scc.read_binary(EF_USIM_ADF_map['FPLMN'])
- if sw == '9000':
- return format_xplmn(res), sw
- else:
- return None, sw
-
- def update_fplmn(self, fplmn):
- self._scc.select_file('3f00')
- self.select_adf_by_aid('USIM')
- size = self._scc.binary_size(EF_USIM_ADF_map['FPLMN'])
- encoded = ''.join([enc_plmn(plmn[:3], plmn[3:]) for plmn in fplmn])
- encoded = rpad(encoded, size)
- data, sw = self._scc.update_binary(EF_USIM_ADF_map['FPLMN'], encoded)
- return sw
-
- def read_epdgid(self):
- (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['ePDGId'])
- if sw == '9000':
- try:
- addr, addr_type = dec_addr_tlv(res)
- except:
- addr = None
- addr_type = None
- return (format_addr(addr, addr_type), sw)
- else:
- return (None, sw)
-
- def update_epdgid(self, epdgid):
- size = self._scc.binary_size(EF_USIM_ADF_map['ePDGId']) * 2
- if len(epdgid) > 0:
- addr_type = get_addr_type(epdgid)
- if addr_type == None:
- raise ValueError(
- "Unknown ePDG Id address type or invalid address provided")
- epdgid_tlv = rpad(enc_addr_tlv(epdgid, ('%02x' % addr_type)), size)
- else:
- epdgid_tlv = rpad('ff', size)
- data, sw = self._scc.update_binary(
- EF_USIM_ADF_map['ePDGId'], epdgid_tlv)
- return sw
-
- def read_ePDGSelection(self):
- (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['ePDGSelection'])
- if sw == '9000':
- return (format_ePDGSelection(res), sw)
- else:
- return (None, sw)
-
- def update_ePDGSelection(self, mcc, mnc):
- (res, sw) = self._scc.read_binary(
- EF_USIM_ADF_map['ePDGSelection'], length=None, offset=0)
- if sw == '9000' and (len(mcc) == 0 or len(mnc) == 0):
- # Reset contents
- # 80 - Tag value
- (res, sw) = self._scc.update_binary(
- EF_USIM_ADF_map['ePDGSelection'], rpad('', len(res)))
- elif sw == '9000':
- (res, sw) = self._scc.update_binary(
- EF_USIM_ADF_map['ePDGSelection'], enc_ePDGSelection(res, mcc, mnc))
- return sw
-
- def read_ust(self):
- (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['UST'])
- if sw == '9000':
- # Print those which are available
- return ([res, dec_st(res, table="usim")], sw)
- else:
- return ([None, None], sw)
-
- def update_ust(self, service, bit=1):
- (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['UST'])
- if sw == '9000':
- content = enc_st(res, service, bit)
- (res, sw) = self._scc.update_binary(
- EF_USIM_ADF_map['UST'], content)
- return sw
-
- def update_est(self, service, bit=1):
- (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['EST'])
- if sw == '9000':
- content = enc_st(res, service, bit)
- (res, sw) = self._scc.update_binary(
- EF_USIM_ADF_map['EST'], content)
- return sw
-
-
-
-class IsimCard(SimCard):
-
- name = 'ISIM'
-
- def __init__(self, ssc):
- super(IsimCard, self).__init__(ssc)
-
- def read_pcscf(self):
- rec_cnt = self._scc.record_count(EF_ISIM_ADF_map['PCSCF'])
- pcscf_recs = ""
- for i in range(0, rec_cnt):
- (res, sw) = self._scc.read_record(EF_ISIM_ADF_map['PCSCF'], i + 1)
- if sw == '9000':
- try:
- addr, addr_type = dec_addr_tlv(res)
- except:
- addr = None
- addr_type = None
- content = format_addr(addr, addr_type)
- pcscf_recs += "%s" % (len(content)
- and content or '\tNot available\n')
- else:
- pcscf_recs += "\tP-CSCF: Can't read, response code = %s\n" % (
- sw)
- return pcscf_recs
-
- def update_pcscf(self, pcscf):
- if len(pcscf) > 0:
- addr_type = get_addr_type(pcscf)
- if addr_type == None:
- raise ValueError(
- "Unknown PCSCF address type or invalid address provided")
- content = enc_addr_tlv(pcscf, ('%02x' % addr_type))
- else:
- # Just the tag value
- content = '80'
- rec_size_bytes = self._scc.record_size(EF_ISIM_ADF_map['PCSCF'])
- pcscf_tlv = rpad(content, rec_size_bytes*2)
- data, sw = self._scc.update_record(
- EF_ISIM_ADF_map['PCSCF'], 1, pcscf_tlv)
- return sw
-
- def read_domain(self):
- (res, sw) = self._scc.read_binary(EF_ISIM_ADF_map['DOMAIN'])
- if sw == '9000':
- # Skip the initial tag value ('80') byte and get length of contents
- length = int(res[2:4], 16)
- content = h2s(res[4:4+(length*2)])
- return (content, sw)
- else:
- return (None, sw)
-
- def update_domain(self, domain=None, mcc=None, mnc=None):
- hex_str = ""
- if domain:
- hex_str = s2h(domain)
- elif mcc and mnc:
- # MCC and MNC always has 3 digits in domain form
- plmn_str = 'mnc' + lpad(mnc, 3, "0") + '.mcc' + lpad(mcc, 3, "0")
- hex_str = s2h('ims.' + plmn_str + '.3gppnetwork.org')
-
- # Build TLV
- tlv = TLV(['80'])
- content = tlv.build({'80': hex_str})
-
- bin_size_bytes = self._scc.binary_size(EF_ISIM_ADF_map['DOMAIN'])
- data, sw = self._scc.update_binary(
- EF_ISIM_ADF_map['DOMAIN'], rpad(content, bin_size_bytes*2))
- return sw
-
- def read_impi(self):
- (res, sw) = self._scc.read_binary(EF_ISIM_ADF_map['IMPI'])
- if sw == '9000':
- # Skip the initial tag value ('80') byte and get length of contents
- length = int(res[2:4], 16)
- content = h2s(res[4:4+(length*2)])
- return (content, sw)
- else:
- return (None, sw)
-
- def update_impi(self, impi=None):
- hex_str = ""
- if impi:
- hex_str = s2h(impi)
- # Build TLV
- tlv = TLV(['80'])
- content = tlv.build({'80': hex_str})
-
- bin_size_bytes = self._scc.binary_size(EF_ISIM_ADF_map['IMPI'])
- data, sw = self._scc.update_binary(
- EF_ISIM_ADF_map['IMPI'], rpad(content, bin_size_bytes*2))
- return sw
-
- def read_impu(self):
- rec_cnt = self._scc.record_count(EF_ISIM_ADF_map['IMPU'])
- impu_recs = ""
- for i in range(0, rec_cnt):
- (res, sw) = self._scc.read_record(EF_ISIM_ADF_map['IMPU'], i + 1)
- if sw == '9000':
- # Skip the initial tag value ('80') byte and get length of contents
- length = int(res[2:4], 16)
- content = h2s(res[4:4+(length*2)])
- impu_recs += "\t%s\n" % (len(content)
- and content or 'Not available')
- else:
- impu_recs += "IMS public user identity: Can't read, response code = %s\n" % (
- sw)
- return impu_recs
-
- def update_impu(self, impu=None):
- hex_str = ""
- if impu:
- hex_str = s2h(impu)
- # Build TLV
- tlv = TLV(['80'])
- content = tlv.build({'80': hex_str})
-
- rec_size_bytes = self._scc.record_size(EF_ISIM_ADF_map['IMPU'])
- impu_tlv = rpad(content, rec_size_bytes*2)
- data, sw = self._scc.update_record(
- EF_ISIM_ADF_map['IMPU'], 1, impu_tlv)
- return sw
-
- def read_iari(self):
- rec_cnt = self._scc.record_count(EF_ISIM_ADF_map['UICCIARI'])
- uiari_recs = ""
- for i in range(0, rec_cnt):
- (res, sw) = self._scc.read_record(
- EF_ISIM_ADF_map['UICCIARI'], i + 1)
- if sw == '9000':
- # Skip the initial tag value ('80') byte and get length of contents
- length = int(res[2:4], 16)
- content = h2s(res[4:4+(length*2)])
- uiari_recs += "\t%s\n" % (len(content)
- and content or 'Not available')
- else:
- uiari_recs += "UICC IARI: Can't read, response code = %s\n" % (
- sw)
- return uiari_recs
-
- def update_ist(self, service, bit=1):
- (res, sw) = self._scc.read_binary(EF_ISIM_ADF_map['IST'])
- if sw == '9000':
- content = enc_st(res, service, bit)
- (res, sw) = self._scc.update_binary(
- EF_ISIM_ADF_map['IST'], content)
- return sw
-
-
-class MagicSimBase(abc.ABC, SimCard):
- """
- Theses cards uses several record based EFs to store the provider infos,
- each possible provider uses a specific record number in each EF. The
- indexes used are ( where N is the number of providers supported ) :
- - [2 .. N+1] for the operator name
- - [1 .. N] for the programmable EFs
-
- * 3f00/7f4d/8f0c : Operator Name
-
- bytes 0-15 : provider name, padded with 0xff
- byte 16 : length of the provider name
- byte 17 : 01 for valid records, 00 otherwise
-
- * 3f00/7f4d/8f0d : Programmable Binary EFs
-
- * 3f00/7f4d/8f0e : Programmable Record EFs
-
- """
-
- _files = {} # type: Dict[str, Tuple[str, int, bool]]
- _ki_file = None # type: Optional[str]
-
- @classmethod
- def autodetect(kls, scc):
- try:
- for p, l, t in kls._files.values():
- if not t:
- continue
- if scc.record_size(['3f00', '7f4d', p]) != l:
- return None
- except:
- return None
-
- return kls(scc)
-
- def _get_count(self):
- """
- Selects the file and returns the total number of entries
- and entry size
- """
- f = self._files['name']
-
- r = self._scc.select_path(['3f00', '7f4d', f[0]])
- rec_len = int(r[-1][28:30], 16)
- tlen = int(r[-1][4:8], 16)
- rec_cnt = (tlen // rec_len) - 1
-
- if (rec_cnt < 1) or (rec_len != f[1]):
- raise RuntimeError('Bad card type')
-
- return rec_cnt
-
- def program(self, p):
- # Go to dir
- self._scc.select_path(['3f00', '7f4d'])
-
- # Home PLMN in PLMN_Sel format
- hplmn = enc_plmn(p['mcc'], p['mnc'])
-
- # Operator name ( 3f00/7f4d/8f0c )
- self._scc.update_record(self._files['name'][0], 2,
- rpad(b2h(p['name']), 32) + ('%02x' %
- len(p['name'])) + '01'
- )
-
- # ICCID/IMSI/Ki/HPLMN ( 3f00/7f4d/8f0d )
- v = ''
-
- # inline Ki
- if self._ki_file is None:
- v += p['ki']
-
- # ICCID
- v += '3f00' + '2fe2' + '0a' + enc_iccid(p['iccid'])
-
- # IMSI
- v += '7f20' + '6f07' + '09' + enc_imsi(p['imsi'])
-
- # Ki
- if self._ki_file:
- v += self._ki_file + '10' + p['ki']
-
- # PLMN_Sel
- v += '6f30' + '18' + rpad(hplmn, 36)
-
- # ACC
- # This doesn't work with "fake" SuperSIM cards,
- # but will hopefully work with real SuperSIMs.
- if p.get('acc') is not None:
- v += '6f78' + '02' + lpad(p['acc'], 4)
-
- self._scc.update_record(self._files['b_ef'][0], 1,
- rpad(v, self._files['b_ef'][1]*2)
- )
-
- # SMSP ( 3f00/7f4d/8f0e )
- # FIXME
-
- # Write PLMN_Sel forcefully as well
- r = self._scc.select_path(['3f00', '7f20', '6f30'])
- tl = int(r[-1][4:8], 16)
-
- hplmn = enc_plmn(p['mcc'], p['mnc'])
- self._scc.update_binary('6f30', hplmn + 'ff' * (tl-3))
-
- def erase(self):
- # Dummy
- df = {}
- for k, v in self._files.items():
- ofs = 1
- fv = v[1] * 'ff'
- if k == 'name':
- ofs = 2
- fv = fv[0:-4] + '0000'
- df[v[0]] = (fv, ofs)
-
- # Write
- for n in range(0, self._get_count()):
- for k, (msg, ofs) in df.items():
- self._scc.update_record(['3f00', '7f4d', k], n + ofs, msg)
-
-
-class SuperSim(MagicSimBase):
-
- name = 'supersim'
-
- _files = {
- 'name': ('8f0c', 18, True),
- 'b_ef': ('8f0d', 74, True),
- 'r_ef': ('8f0e', 50, True),
- }
-
- _ki_file = None
-
-
-class MagicSim(MagicSimBase):
-
- name = 'magicsim'
-
- _files = {
- 'name': ('8f0c', 18, True),
- 'b_ef': ('8f0d', 130, True),
- 'r_ef': ('8f0e', 102, False),
- }
-
- _ki_file = '6f1b'
-
-
-class FakeMagicSim(SimCard):
- """
- Theses cards have a record based EF 3f00/000c that contains the provider
- information. See the program method for its format. The records go from
- 1 to N.
- """
-
- name = 'fakemagicsim'
-
- @classmethod
- def autodetect(kls, scc):
- try:
- if scc.record_size(['3f00', '000c']) != 0x5a:
- return None
- except:
- return None
-
- return kls(scc)
-
- def _get_infos(self):
- """
- Selects the file and returns the total number of entries
- and entry size
- """
-
- r = self._scc.select_path(['3f00', '000c'])
- rec_len = int(r[-1][28:30], 16)
- tlen = int(r[-1][4:8], 16)
- rec_cnt = (tlen // rec_len) - 1
-
- if (rec_cnt < 1) or (rec_len != 0x5a):
- raise RuntimeError('Bad card type')
-
- return rec_cnt, rec_len
-
- def program(self, p):
- # Home PLMN
- r = self._scc.select_path(['3f00', '7f20', '6f30'])
- tl = int(r[-1][4:8], 16)
-
- hplmn = enc_plmn(p['mcc'], p['mnc'])
- self._scc.update_binary('6f30', hplmn + 'ff' * (tl-3))
-
- # Get total number of entries and entry size
- rec_cnt, rec_len = self._get_infos()
-
- # Set first entry
- entry = (
- '81' + # 1b Status: Valid & Active
- rpad(s2h(p['name'][0:14]), 28) + # 14b Entry Name
- enc_iccid(p['iccid']) + # 10b ICCID
- enc_imsi(p['imsi']) + # 9b IMSI_len + id_type(9) + IMSI
- p['ki'] + # 16b Ki
- lpad(p['smsp'], 80) # 40b SMSP (padded with ff if needed)
- )
- self._scc.update_record('000c', 1, entry)
-
- def erase(self):
- # Get total number of entries and entry size
- rec_cnt, rec_len = self._get_infos()
-
- # Erase all entries
- entry = 'ff' * rec_len
- for i in range(0, rec_cnt):
- self._scc.update_record('000c', 1+i, entry)
-
-
-class GrcardSim(SimCard):
- """
- Greencard (grcard.cn) HZCOS GSM SIM
- These cards have a much more regular ISO 7816-4 / TS 11.11 structure,
- and use standard UPDATE RECORD / UPDATE BINARY commands except for Ki.
- """
-
- name = 'grcardsim'
-
- @classmethod
- def autodetect(kls, scc):
- return None
-
- def program(self, p):
- # We don't really know yet what ADM PIN 4 is about
- #self._scc.verify_chv(4, h2b("4444444444444444"))
-
- # Authenticate using ADM PIN 5
- if p['pin_adm']:
- pin = h2b(p['pin_adm'])
- else:
- pin = h2b("4444444444444444")
- self._scc.verify_chv(5, pin)
-
- # EF.ICCID
- r = self._scc.select_path(['3f00', '2fe2'])
- data, sw = self._scc.update_binary('2fe2', enc_iccid(p['iccid']))
-
- # EF.IMSI
- r = self._scc.select_path(['3f00', '7f20', '6f07'])
- data, sw = self._scc.update_binary('6f07', enc_imsi(p['imsi']))
-
- # EF.ACC
- if p.get('acc') is not None:
- data, sw = self._scc.update_binary('6f78', lpad(p['acc'], 4))
-
- # EF.SMSP
- if p.get('smsp'):
- r = self._scc.select_path(['3f00', '7f10', '6f42'])
- data, sw = self._scc.update_record('6f42', 1, lpad(p['smsp'], 80))
-
- # Set the Ki using proprietary command
- pdu = '80d4020010' + p['ki']
- data, sw = self._scc._tp.send_apdu(pdu)
-
- # EF.HPLMN
- r = self._scc.select_path(['3f00', '7f20', '6f30'])
- size = int(r[-1][4:8], 16)
- hplmn = enc_plmn(p['mcc'], p['mnc'])
- self._scc.update_binary('6f30', hplmn + 'ff' * (size-3))
-
- # EF.SPN (Service Provider Name)
- r = self._scc.select_path(['3f00', '7f20', '6f30'])
- size = int(r[-1][4:8], 16)
- # FIXME
-
- # FIXME: EF.MSISDN
-
-
-class SysmoSIMgr1(GrcardSim):
- """
- sysmocom sysmoSIM-GR1
- These cards have a much more regular ISO 7816-4 / TS 11.11 structure,
- and use standard UPDATE RECORD / UPDATE BINARY commands except for Ki.
- """
- name = 'sysmosim-gr1'
-
- @classmethod
- def autodetect(kls, scc):
- try:
- # Look for ATR
- if scc.get_atr() == toBytes("3B 99 18 00 11 88 22 33 44 55 66 77 60"):
- return kls(scc)
- except:
- return None
- return None
-
-
-class SysmoUSIMgr1(UsimCard):
- """
- sysmocom sysmoUSIM-GR1
- """
- name = 'sysmoUSIM-GR1'
-
- @classmethod
- def autodetect(kls, scc):
- # TODO: Access the ATR
- return None
-
- def program(self, p):
- # TODO: check if verify_chv could be used or what it needs
- # self._scc.verify_chv(0x0A, [0x33,0x32,0x32,0x31,0x33,0x32,0x33,0x32])
- # Unlock the card..
- data, sw = self._scc._tp.send_apdu_checksw(
- "0020000A083332323133323332")
-
- # TODO: move into SimCardCommands
- par = (p['ki'] + # 16b K
- p['opc'] + # 32b OPC
- enc_iccid(p['iccid']) + # 10b ICCID
- enc_imsi(p['imsi']) # 9b IMSI_len + id_type(9) + IMSI
- )
- data, sw = self._scc._tp.send_apdu_checksw("0099000033" + par)
-
-
-class SysmoSIMgr2(SimCard):
- """
- sysmocom sysmoSIM-GR2
- """
-
- name = 'sysmoSIM-GR2'
-
- @classmethod
- def autodetect(kls, scc):
- try:
- # Look for ATR
- if scc.get_atr() == toBytes("3B 7D 94 00 00 55 55 53 0A 74 86 93 0B 24 7C 4D 54 68"):
- return kls(scc)
- except:
- return None
- return None
-
- def program(self, p):
-
- # select MF
- r = self._scc.select_path(['3f00'])
-
- # authenticate as SUPER ADM using default key
- self._scc.verify_chv(0x0b, h2b("3838383838383838"))
-
- # set ADM pin using proprietary command
- # INS: D4
- # P1: 3A for PIN, 3B for PUK
- # P2: CHV number, as in VERIFY CHV for PIN, and as in UNBLOCK CHV for PUK
- # P3: 08, CHV length (curiously the PUK is also 08 length, instead of 10)
- if p['pin_adm']:
- pin = h2b(p['pin_adm'])
- else:
- pin = h2b("4444444444444444")
-
- pdu = 'A0D43A0508' + b2h(pin)
- data, sw = self._scc._tp.send_apdu(pdu)
-
- # authenticate as ADM (enough to write file, and can set PINs)
-
- self._scc.verify_chv(0x05, pin)
-
- # write EF.ICCID
- data, sw = self._scc.update_binary('2fe2', enc_iccid(p['iccid']))
-
- # select DF_GSM
- r = self._scc.select_path(['7f20'])
-
- # write EF.IMSI
- data, sw = self._scc.update_binary('6f07', enc_imsi(p['imsi']))
-
- # write EF.ACC
- if p.get('acc') is not None:
- data, sw = self._scc.update_binary('6f78', lpad(p['acc'], 4))
-
- # get size and write EF.HPLMN
- r = self._scc.select_path(['6f30'])
- size = int(r[-1][4:8], 16)
- hplmn = enc_plmn(p['mcc'], p['mnc'])
- self._scc.update_binary('6f30', hplmn + 'ff' * (size-3))
-
- # set COMP128 version 0 in proprietary file
- data, sw = self._scc.update_binary('0001', '001000')
-
- # set Ki in proprietary file
- data, sw = self._scc.update_binary('0001', p['ki'], 3)
-
- # select DF_TELECOM
- r = self._scc.select_path(['3f00', '7f10'])
-
- # write EF.SMSP
- if p.get('smsp'):
- data, sw = self._scc.update_record('6f42', 1, lpad(p['smsp'], 80))
-
-
-class SysmoUSIMSJS1(UsimCard):
- """
- sysmocom sysmoUSIM-SJS1
- """
-
- name = 'sysmoUSIM-SJS1'
-
- def __init__(self, ssc):
- super(SysmoUSIMSJS1, self).__init__(ssc)
- self._scc.cla_byte = "00"
- self._scc.sel_ctrl = "0004" # request an FCP
-
- @classmethod
- def autodetect(kls, scc):
- try:
- # Look for ATR
- if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 43 20 07 18 00 00 01 A5"):
- return kls(scc)
- except:
- return None
- return None
-
- def verify_adm(self, key):
- # authenticate as ADM using default key (written on the card..)
- if not key:
- raise ValueError(
- "Please provide a PIN-ADM as there is no default one")
- (res, sw) = self._scc.verify_chv(0x0A, key)
- return sw
-
- def program(self, p):
- self.verify_adm(h2b(p['pin_adm']))
-
- # select MF
- r = self._scc.select_path(['3f00'])
-
- # write EF.ICCID
- data, sw = self._scc.update_binary('2fe2', enc_iccid(p['iccid']))
-
- # select DF_GSM
- r = self._scc.select_path(['7f20'])
-
- # set Ki in proprietary file
- data, sw = self._scc.update_binary('00FF', p['ki'])
-
- # set OPc in proprietary file
- if 'opc' in p:
- content = "01" + p['opc']
- data, sw = self._scc.update_binary('00F7', content)
-
- # set Service Provider Name
- if p.get('name') is not None:
- self.update_spn(p['name'], True, True)
-
- if p.get('acc') is not None:
- self.update_acc(p['acc'])
-
- # write EF.IMSI
- data, sw = self._scc.update_binary('6f07', enc_imsi(p['imsi']))
-
- # EF.PLMNsel
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_plmnsel(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming PLMNsel failed with code %s" % sw)
-
- # EF.PLMNwAcT
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_plmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming PLMNwAcT failed with code %s" % sw)
-
- # EF.OPLMNwAcT
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_oplmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming OPLMNwAcT failed with code %s" % sw)
-
- # EF.HPLMNwAcT
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_hplmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming HPLMNwAcT failed with code %s" % sw)
-
- # EF.AD
- if (p.get('mcc') and p.get('mnc')) or p.get('opmode'):
- if p.get('mcc') and p.get('mnc'):
- mnc = p['mnc']
- else:
- mnc = None
- sw = self.update_ad(mnc=mnc, opmode=p.get('opmode'))
- if sw != '9000':
- print("Programming AD failed with code %s" % sw)
-
- # EF.SMSP
- if p.get('smsp'):
- r = self._scc.select_path(['3f00', '7f10'])
- data, sw = self._scc.update_record(
- '6f42', 1, lpad(p['smsp'], 104), force_len=True)
-
- # EF.MSISDN
- # TODO: Alpha Identifier (currently 'ff'O * 20)
- # TODO: Capability/Configuration1 Record Identifier
- # TODO: Extension1 Record Identifier
- if p.get('msisdn') is not None:
- msisdn = enc_msisdn(p['msisdn'])
- data = 'ff' * 20 + msisdn
-
- r = self._scc.select_path(['3f00', '7f10'])
- data, sw = self._scc.update_record('6F40', 1, data, force_len=True)
-
-
-class FairwavesSIM(UsimCard):
- """
- FairwavesSIM
-
- The SIM card is operating according to the standard.
- For Ki/OP/OPC programming the following files are additionally open for writing:
- 3F00/7F20/FF01 – OP/OPC:
- byte 1 = 0x01, bytes 2-17: OPC;
- byte 1 = 0x00, bytes 2-17: OP;
- 3F00/7F20/FF02: Ki
- """
-
- name = 'Fairwaves-SIM'
- # Propriatary files
- _EF_num = {
- 'Ki': 'FF02',
- 'OP/OPC': 'FF01',
- }
- _EF = {
- 'Ki': DF['GSM']+[_EF_num['Ki']],
- 'OP/OPC': DF['GSM']+[_EF_num['OP/OPC']],
- }
-
- def __init__(self, ssc):
- super(FairwavesSIM, self).__init__(ssc)
- self._adm_chv_num = 0x11
- self._adm2_chv_num = 0x12
-
- @classmethod
- def autodetect(kls, scc):
- try:
- # Look for ATR
- if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 44 22 06 10 00 00 01 A9"):
- return kls(scc)
- except:
- return None
- return None
-
- def verify_adm2(self, key):
- '''
- Authenticate with ADM2 key.
-
- Fairwaves SIM cards support hierarchical key structure and ADM2 key
- is a key which has access to proprietary files (Ki and OP/OPC).
- That said, ADM key inherits permissions of ADM2 key and thus we rarely
- need ADM2 key per se.
- '''
- (res, sw) = self._scc.verify_chv(self._adm2_chv_num, key)
- return sw
-
- def read_ki(self):
- """
- Read Ki in proprietary file.
-
- Requires ADM1 access level
- """
- return self._scc.read_binary(self._EF['Ki'])
-
- def update_ki(self, ki):
- """
- Set Ki in proprietary file.
-
- Requires ADM1 access level
- """
- data, sw = self._scc.update_binary(self._EF['Ki'], ki)
- return sw
-
- def read_op_opc(self):
- """
- Read Ki in proprietary file.
-
- Requires ADM1 access level
- """
- (ef, sw) = self._scc.read_binary(self._EF['OP/OPC'])
- type = 'OP' if ef[0:2] == '00' else 'OPC'
- return ((type, ef[2:]), sw)
-
- def update_op(self, op):
- """
- Set OP in proprietary file.
-
- Requires ADM1 access level
- """
- content = '00' + op
- data, sw = self._scc.update_binary(self._EF['OP/OPC'], content)
- return sw
-
- def update_opc(self, opc):
- """
- Set OPC in proprietary file.
-
- Requires ADM1 access level
- """
- content = '01' + opc
- data, sw = self._scc.update_binary(self._EF['OP/OPC'], content)
- return sw
-
- def program(self, p):
- # For some reason the card programming only works when the card
- # is handled as a classic SIM, even though it is an USIM, so we
- # reconfigure the class byte and the select control field on
- # the fly. When the programming is done the original values are
- # restored.
- cla_byte_orig = self._scc.cla_byte
- sel_ctrl_orig = self._scc.sel_ctrl
- self._scc.cla_byte = "a0"
- self._scc.sel_ctrl = "0000"
-
- try:
- self._program(p)
- finally:
- # restore original cla byte and sel ctrl
- self._scc.cla_byte = cla_byte_orig
- self._scc.sel_ctrl = sel_ctrl_orig
-
- def _program(self, p):
- # authenticate as ADM1
- if not p['pin_adm']:
- raise ValueError(
- "Please provide a PIN-ADM as there is no default one")
- self.verify_adm(h2b(p['pin_adm']))
-
- # TODO: Set operator name
- if p.get('smsp') is not None:
- sw = self.update_smsp(p['smsp'])
- if sw != '9000':
- print("Programming SMSP failed with code %s" % sw)
- # This SIM doesn't support changing ICCID
- if p.get('mcc') is not None and p.get('mnc') is not None:
- sw = self.update_hplmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming MCC/MNC failed with code %s" % sw)
- if p.get('imsi') is not None:
- sw = self.update_imsi(p['imsi'])
- if sw != '9000':
- print("Programming IMSI failed with code %s" % sw)
- if p.get('ki') is not None:
- sw = self.update_ki(p['ki'])
- if sw != '9000':
- print("Programming Ki failed with code %s" % sw)
- if p.get('opc') is not None:
- sw = self.update_opc(p['opc'])
- if sw != '9000':
- print("Programming OPC failed with code %s" % sw)
- if p.get('acc') is not None:
- sw = self.update_acc(p['acc'])
- if sw != '9000':
- print("Programming ACC failed with code %s" % sw)
-
-
-class OpenCellsSim(SimCard):
- """
- OpenCellsSim
-
- """
-
- name = 'OpenCells-SIM'
-
- def __init__(self, ssc):
- super(OpenCellsSim, self).__init__(ssc)
- self._adm_chv_num = 0x0A
-
- @classmethod
- def autodetect(kls, scc):
- try:
- # Look for ATR
- if scc.get_atr() == toBytes("3B 9F 95 80 1F C3 80 31 E0 73 FE 21 13 57 86 81 02 86 98 44 18 A8"):
- return kls(scc)
- except:
- return None
- return None
-
- def program(self, p):
- if not p['pin_adm']:
- raise ValueError(
- "Please provide a PIN-ADM as there is no default one")
- self._scc.verify_chv(0x0A, h2b(p['pin_adm']))
-
- # select MF
- r = self._scc.select_path(['3f00'])
-
- # write EF.ICCID
- data, sw = self._scc.update_binary('2fe2', enc_iccid(p['iccid']))
-
- r = self._scc.select_path(['7ff0'])
-
- # set Ki in proprietary file
- data, sw = self._scc.update_binary('FF02', p['ki'])
-
- # set OPC in proprietary file
- data, sw = self._scc.update_binary('FF01', p['opc'])
-
- # select DF_GSM
- r = self._scc.select_path(['7f20'])
-
- # write EF.IMSI
- data, sw = self._scc.update_binary('6f07', enc_imsi(p['imsi']))
-
-
-class WavemobileSim(UsimCard):
- """
- WavemobileSim
-
- """
-
- name = 'Wavemobile-SIM'
-
- def __init__(self, ssc):
- super(WavemobileSim, self).__init__(ssc)
- self._adm_chv_num = 0x0A
- self._scc.cla_byte = "00"
- self._scc.sel_ctrl = "0004" # request an FCP
-
- @classmethod
- def autodetect(kls, scc):
- try:
- # Look for ATR
- if scc.get_atr() == toBytes("3B 9F 95 80 1F C7 80 31 E0 73 F6 21 13 67 4D 45 16 00 43 01 00 8F"):
- return kls(scc)
- except:
- return None
- return None
-
- def program(self, p):
- if not p['pin_adm']:
- raise ValueError(
- "Please provide a PIN-ADM as there is no default one")
- self.verify_adm(h2b(p['pin_adm']))
-
- # EF.ICCID
- # TODO: Add programming of the ICCID
- if p.get('iccid'):
- print(
- "Warning: Programming of the ICCID is not implemented for this type of card.")
-
- # KI (Presumably a proprietary file)
- # TODO: Add programming of KI
- if p.get('ki'):
- print(
- "Warning: Programming of the KI is not implemented for this type of card.")
-
- # OPc (Presumably a proprietary file)
- # TODO: Add programming of OPc
- if p.get('opc'):
- print(
- "Warning: Programming of the OPc is not implemented for this type of card.")
-
- # EF.SMSP
- if p.get('smsp'):
- sw = self.update_smsp(p['smsp'])
- if sw != '9000':
- print("Programming SMSP failed with code %s" % sw)
-
- # EF.IMSI
- if p.get('imsi'):
- sw = self.update_imsi(p['imsi'])
- if sw != '9000':
- print("Programming IMSI failed with code %s" % sw)
-
- # EF.ACC
- if p.get('acc'):
- sw = self.update_acc(p['acc'])
- if sw != '9000':
- print("Programming ACC failed with code %s" % sw)
-
- # EF.PLMNsel
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_plmnsel(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming PLMNsel failed with code %s" % sw)
-
- # EF.PLMNwAcT
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_plmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming PLMNwAcT failed with code %s" % sw)
-
- # EF.OPLMNwAcT
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_oplmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming OPLMNwAcT failed with code %s" % sw)
-
- # EF.AD
- if (p.get('mcc') and p.get('mnc')) or p.get('opmode'):
- if p.get('mcc') and p.get('mnc'):
- mnc = p['mnc']
- else:
- mnc = None
- sw = self.update_ad(mnc=mnc, opmode=p.get('opmode'))
- if sw != '9000':
- print("Programming AD failed with code %s" % sw)
-
- return None
-
-
-class SysmoISIMSJA2(UsimCard, IsimCard):
- """
- sysmocom sysmoISIM-SJA2
- """
-
- name = 'sysmoISIM-SJA2'
-
- def __init__(self, ssc):
- super(SysmoISIMSJA2, self).__init__(ssc)
- self._scc.cla_byte = "00"
- self._scc.sel_ctrl = "0004" # request an FCP
-
- @classmethod
- def autodetect(kls, scc):
- try:
- # Try card model #1
- atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9"
- if scc.get_atr() == toBytes(atr):
- return kls(scc)
-
- # Try card model #2
- atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2"
- if scc.get_atr() == toBytes(atr):
- return kls(scc)
-
- # Try card model #3
- atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"
- if scc.get_atr() == toBytes(atr):
- return kls(scc)
- except:
- return None
- return None
-
- def verify_adm(self, key):
- # authenticate as ADM using default key (written on the card..)
- if not key:
- raise ValueError(
- "Please provide a PIN-ADM as there is no default one")
- (res, sw) = self._scc.verify_chv(0x0A, key)
- return sw
-
- def program(self, p):
- self.verify_adm(h2b(p['pin_adm']))
-
- # Populate AIDs
- self.read_aids()
-
- # This type of card does not allow to reprogram the ICCID.
- # Reprogramming the ICCID would mess up the card os software
- # license management, so the ICCID must be kept at its factory
- # setting!
- if p.get('iccid'):
- print(
- "Warning: Programming of the ICCID is not implemented for this type of card.")
-
- # select DF_GSM
- self._scc.select_path(['7f20'])
-
- # set Service Provider Name
- if p.get('name') is not None:
- self.update_spn(p['name'], True, True)
-
- # write EF.IMSI
- if p.get('imsi'):
- self._scc.update_binary('6f07', enc_imsi(p['imsi']))
-
- # EF.PLMNsel
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_plmnsel(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming PLMNsel failed with code %s" % sw)
-
- # EF.PLMNwAcT
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_plmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming PLMNwAcT failed with code %s" % sw)
-
- # EF.OPLMNwAcT
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_oplmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming OPLMNwAcT failed with code %s" % sw)
-
- # EF.HPLMNwAcT
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_hplmn_act(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming HPLMNwAcT failed with code %s" % sw)
-
- # EF.AD
- if (p.get('mcc') and p.get('mnc')) or p.get('opmode'):
- if p.get('mcc') and p.get('mnc'):
- mnc = p['mnc']
- else:
- mnc = None
- sw = self.update_ad(mnc=mnc, opmode=p.get('opmode'))
- if sw != '9000':
- print("Programming AD failed with code %s" % sw)
-
- # EF.SMSP
- if p.get('smsp'):
- r = self._scc.select_path(['3f00', '7f10'])
- data, sw = self._scc.update_record(
- '6f42', 1, lpad(p['smsp'], 104), force_len=True)
-
- # EF.MSISDN
- # TODO: Alpha Identifier (currently 'ff'O * 20)
- # TODO: Capability/Configuration1 Record Identifier
- # TODO: Extension1 Record Identifier
- if p.get('msisdn') is not None:
- msisdn = enc_msisdn(p['msisdn'])
- content = 'ff' * 20 + msisdn
-
- r = self._scc.select_path(['3f00', '7f10'])
- data, sw = self._scc.update_record(
- '6F40', 1, content, force_len=True)
-
- # EF.ACC
- if p.get('acc'):
- sw = self.update_acc(p['acc'])
- if sw != '9000':
- print("Programming ACC failed with code %s" % sw)
-
- # update EF-SIM_AUTH_KEY (and EF-USIM_AUTH_KEY_2G, which is
- # hard linked to EF-USIM_AUTH_KEY)
- self._scc.select_path(['3f00'])
- self._scc.select_path(['a515'])
- if p.get('ki'):
- self._scc.update_binary('6f20', p['ki'], 1)
- if p.get('opc'):
- self._scc.update_binary('6f20', p['opc'], 17)
-
- # update EF-USIM_AUTH_KEY in ADF.ISIM
- if self.adf_present("isim"):
- self.select_adf_by_aid(adf="isim")
-
- if p.get('ki'):
- self._scc.update_binary('af20', p['ki'], 1)
- if p.get('opc'):
- self._scc.update_binary('af20', p['opc'], 17)
-
- # update EF.P-CSCF in ADF.ISIM
- if self.file_exists(EF_ISIM_ADF_map['PCSCF']):
- if p.get('pcscf'):
- sw = self.update_pcscf(p['pcscf'])
- else:
- sw = self.update_pcscf("")
- if sw != '9000':
- print("Programming P-CSCF failed with code %s" % sw)
-
- # update EF.DOMAIN in ADF.ISIM
- if self.file_exists(EF_ISIM_ADF_map['DOMAIN']):
- if p.get('ims_hdomain'):
- sw = self.update_domain(domain=p['ims_hdomain'])
- else:
- sw = self.update_domain()
-
- if sw != '9000':
- print(
- "Programming Home Network Domain Name failed with code %s" % sw)
-
- # update EF.IMPI in ADF.ISIM
- # TODO: Validate IMPI input
- if self.file_exists(EF_ISIM_ADF_map['IMPI']):
- if p.get('impi'):
- sw = self.update_impi(p['impi'])
- else:
- sw = self.update_impi()
- if sw != '9000':
- print("Programming IMPI failed with code %s" % sw)
-
- # update EF.IMPU in ADF.ISIM
- # TODO: Validate IMPU input
- # Support multiple IMPU if there is enough space
- if self.file_exists(EF_ISIM_ADF_map['IMPU']):
- if p.get('impu'):
- sw = self.update_impu(p['impu'])
- else:
- sw = self.update_impu()
- if sw != '9000':
- print("Programming IMPU failed with code %s" % sw)
-
- if self.adf_present("usim"):
- self.select_adf_by_aid(adf="usim")
-
- # EF.AD in ADF.USIM
- if (p.get('mcc') and p.get('mnc')) or p.get('opmode'):
- if p.get('mcc') and p.get('mnc'):
- mnc = p['mnc']
- else:
- mnc = None
- sw = self.update_ad(mnc=mnc, opmode=p.get('opmode'),
- path=EF_USIM_ADF_map['AD'])
- if sw != '9000':
- print("Programming AD failed with code %s" % sw)
-
- # update EF-USIM_AUTH_KEY in ADF.USIM
- if p.get('ki'):
- self._scc.update_binary('af20', p['ki'], 1)
- if p.get('opc'):
- self._scc.update_binary('af20', p['opc'], 17)
-
- # update EF.EHPLMN in ADF.USIM
- if self.file_exists(EF_USIM_ADF_map['EHPLMN']):
- if p.get('mcc') and p.get('mnc'):
- sw = self.update_ehplmn(p['mcc'], p['mnc'])
- if sw != '9000':
- print("Programming EHPLMN failed with code %s" % sw)
-
- # update EF.ePDGId in ADF.USIM
- if self.file_exists(EF_USIM_ADF_map['ePDGId']):
- if p.get('epdgid'):
- sw = self.update_epdgid(p['epdgid'])
- else:
- sw = self.update_epdgid("")
- if sw != '9000':
- print("Programming ePDGId failed with code %s" % sw)
-
- # update EF.ePDGSelection in ADF.USIM
- if self.file_exists(EF_USIM_ADF_map['ePDGSelection']):
- if p.get('epdgSelection'):
- epdg_plmn = p['epdgSelection']
- sw = self.update_ePDGSelection(
- epdg_plmn[:3], epdg_plmn[3:])
- else:
- sw = self.update_ePDGSelection("", "")
- if sw != '9000':
- print("Programming ePDGSelection failed with code %s" % sw)
-
- # After successfully programming EF.ePDGId and EF.ePDGSelection,
- # Set service 106 and 107 as available in EF.UST
- # Disable service 95, 99, 115 if ISIM application is present
- if self.file_exists(EF_USIM_ADF_map['UST']):
- if p.get('epdgSelection') and p.get('epdgid'):
- sw = self.update_ust(106, 1)
- if sw != '9000':
- print("Programming UST failed with code %s" % sw)
- sw = self.update_ust(107, 1)
- if sw != '9000':
- print("Programming UST failed with code %s" % sw)
-
- sw = self.update_ust(95, 0)
- if sw != '9000':
- print("Programming UST failed with code %s" % sw)
- sw = self.update_ust(99, 0)
- if sw != '9000':
- print("Programming UST failed with code %s" % sw)
- sw = self.update_ust(115, 0)
- if sw != '9000':
- print("Programming UST failed with code %s" % sw)
-
- return
-
-class SysmoISIMSJA5(SysmoISIMSJA2):
- """
- sysmocom sysmoISIM-SJA5
- """
-
- name = 'sysmoISIM-SJA5'
-
- @classmethod
- def autodetect(kls, scc):
- try:
- # Try card model #1 (9FJ)
- atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC"
- if scc.get_atr() == toBytes(atr):
- return kls(scc)
- # Try card model #2 (SLM17)
- atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8"
- if scc.get_atr() == toBytes(atr):
- return kls(scc)
- # Try card model #3 (9FV)
- atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"
- if scc.get_atr() == toBytes(atr):
- return kls(scc)
- except:
- return None
- return None
-
-
-class GialerSim(UsimCard):
- """
- Gialer sim cards (www.gialer.com).
- """
- name = 'gialersim'
-
- def __init__(self, ssc):
- super().__init__(ssc)
- self._program_handlers = {
- 'iccid': self.update_iccid,
- 'imsi': self.update_imsi,
- 'acc': self.update_acc,
- 'smsp': self.update_smsp,
- 'ki': self.update_ki,
- 'opc': self.update_opc,
- 'fplmn': self.update_fplmn,
- }
-
- @classmethod
- def autodetect(cls, scc):
- try:
- # Look for ATR
- if scc.get_atr() == toBytes('3B 9F 95 80 1F C7 80 31 A0 73 B6 A1 00 67 CF 32 15 CA 9C D7 09 20'):
- return cls(scc)
- except:
- return None
- return None
-
- def program(self, p):
- self.set_apdu_parameter('00', '0004')
- # Authenticate
- self._scc.verify_chv(0xc, h2b('3834373936313533'))
- for handler in self._program_handlers:
- if p.get(handler) is not None:
- self._program_handlers[handler](p[handler])
-
- mcc = p.get('mcc')
- mnc = p.get('mnc')
- has_plmn = mcc is not None and mnc is not None
- # EF.HPLMN
- if has_plmn:
- self.update_hplmn_act(mcc, mnc)
-
- # EF.AD
- if has_plmn or (p.get('opmode') is not None):
- self.update_ad(mnc=mnc, opmode=p.get('opmode'))
-
- def update_smsp(self, smsp):
- data, sw = self._scc.update_record(EF['SMSP'], 1, rpad(smsp, 80))
- return sw
-
- def update_ki(self, ki):
- self._scc.select_path(['3f00', '0001'])
- self._scc.update_binary('0001', ki)
-
- def update_opc(self, opc):
- self._scc.select_path(['3f00', '6002'])
- # No idea why the '01' is required
- self._scc.update_binary('6002', '01' + opc)
-
-
-# In order for autodetection ...
-_cards_classes = [FakeMagicSim, SuperSim, MagicSim, GrcardSim,
- SysmoSIMgr1, SysmoSIMgr2, SysmoUSIMgr1, SysmoUSIMSJS1,
- FairwavesSIM, OpenCellsSim, WavemobileSim, SysmoISIMSJA2,
- SysmoISIMSJA5, GialerSim]
-
-
-def card_detect(ctype, scc):
- # Detect type if needed
- card = None
- ctypes = dict([(kls.name, kls) for kls in _cards_classes])
-
- if ctype == "auto":
- for kls in _cards_classes:
- card = kls.autodetect(scc)
- if card:
- print("Autodetected card type: %s" % card.name)
- card.reset()
- break
-
- if card is None:
- print("Autodetection failed")
- return None
-
- elif ctype in ctypes:
- card = ctypes[ctype](scc)
+def card_detect(scc: SimCardCommands) -> Optional[CardBase]:
+ # UICC always has higher preference, as a UICC might also contain a SIM application
+ uicc = UiccCardBase(scc)
+ if uicc.probe():
+ return uicc
- else:
- raise ValueError("Unknown card type: %s" % ctype)
+ # this is for detecting a real, classic TS 11.11 SIM card without any UICC support
+ sim = SimCardBase(scc)
+ if sim.probe():
+ return sim
- return card
+ return None
diff --git a/pySim/cat.py b/pySim/cat.py
index e98ce4a..2a830c3 100644
--- a/pySim/cat.py
+++ b/pySim/cat.py
@@ -18,21 +18,21 @@ as described in 3GPP TS 31.111."""
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from bidict import bidict
from typing import List
-from pySim.utils import b2h, h2b, dec_xplmn_w_act
-from pySim.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
-from pySim.construct import BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi
-from construct import Int8ub, Int16ub, Byte, Bytes, Bit, Flag, BitsInteger
-from construct import Struct, Enum, Tell, BitStruct, this, Padding, RepeatUntil
+from bidict import bidict
+from construct import Int8ub, Int16ub, Byte, Bytes, BitsInteger
+from construct import Struct, Enum, BitStruct, this
from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
+from pySim.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
+from pySim.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi
+from pySim.utils import b2h, dec_xplmn_w_act
# Tag values as per TS 101 220 Table 7.23
# TS 102 223 Section 8.1
class Address(COMPR_TLV_IE, tag=0x06):
_construct = Struct('ton_npi'/Int8ub,
- 'call_number'/BcdAdapter(Bytes(this._.total_len-1)))
+ 'call_number'/BcdAdapter(GreedyBytes))
# TS 102 223 Section 8.2
class AlphaIdentifier(COMPR_TLV_IE, tag=0x05):
@@ -583,11 +583,11 @@ class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
# TS 31.111 Section 8.90
class PlmnWactList(COMPR_TLV_IE, tag=0xF2):
- def _from_bytes(self, x):
+ def _from_bytes(self, do: bytes):
r = []
i = 0
- while i < len(x):
- r.append(dec_xplmn_w_act(b2h(x[i:i+5])))
+ while i < len(do):
+ r.append(dec_xplmn_w_act(b2h(do[i:i+5])))
i += 5
return r
@@ -597,7 +597,7 @@ class ContactlessFunctionalityState(COMPR_TLV_IE, tag=0xD4):
# TS 31.111 Section 8.91
class RoutingAreaIdentification(COMPR_TLV_IE, tag=0xF3):
- _construct = Struct('mcc_mnc'/BcdAdapter(Bytes(3)),
+ _construct = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
'lac'/HexAdapter(Bytes(2)),
'rac'/Int8ub)
@@ -645,7 +645,7 @@ class GeographicalLocationParameters(COMPR_TLV_IE, tag=0xF6):
# TS 31.111 Section 8.97
class PlmnList(COMPR_TLV_IE, tag=0xF9):
- _construct = GreedyRange('mcc_mnc'/HexAdapter(Bytes(3)))
+ _construct = GreedyRange('mcc_mnc'/PlmnAdapter(Bytes(3)))
# TS 102 223 Section 8.98
class EcatSequenceNumber(COMPR_TLV_IE, tag=0xA1):
@@ -717,19 +717,193 @@ class SMSCBDownload(BER_TLV_IE, tag=0xD2,
nested=[DeviceIdentities, CBSPage]):
pass
+# TS 101 220 Table 7.17
+class MenuSelection(BER_TLV_IE, tag=0xD3,
+ nested=[DeviceIdentities, ItemIdentifier, HelpRequest]):
+ pass
+
+class BcRepeatIndicator(BER_TLV_IE, tag=0x2A):
+ pass
+
+# TS 101 220 Table 7.17
+class CallControl(BER_TLV_IE, tag=0xD4,
+ nested=[DeviceIdentities, Address, CapabilityConfigParams, Subaddress,
+ LocationInformation, BcRepeatIndicator]):
+ pass
+
+# TS 101 220 Table 7.17
+class MoShortMessageControl(BER_TLV_IE, tag=0xD5):
+ pass
+
+
+# TS 101 220 Table 7.23
+class TransactionIdentifier(BER_TLV_IE, tag=0x1C):
+ pass
+
+# TS 101 220 Table 7.23
+class ImsURI(BER_TLV_IE, tag=0x31):
+ pass
+
+# TS 101 220 Table 7.23
+class UriTruncated(BER_TLV_IE, tag=0x73):
+ pass
+# TS 101 220 Table 7.23
+class TrackingAreaIdentification(BER_TLV_IE, tag=0x7D):
+ pass
+
+# TS 101 220 Table 7.23
+class ExtendedRejectionCauseCode(BER_TLV_IE, tag=0x57):
+ pass
+
+# TS 101 220 Table 7.23
+class CsgCellSelectionStatus(BER_TLV_IE, tag=0x55):
+ pass
+
+# TS 101 220 Table 7.23
+class CsgId(BER_TLV_IE, tag=0x56):
+ pass
+
+# TS 101 220 Table 7.23
+class HnbName(BER_TLV_IE, tag=0x57):
+ pass
+
+# TS 101 220 Table 7.23
+class PlmnId(BER_TLV_IE, tag=0x09):
+ pass
+
+# TS 101 220 Table 7.23
+class ImsCallDisconnectionStatus(BER_TLV_IE, tag=0x55):
+ pass
+
+# TS 101 220 Table 7.23
+class Iari(BER_TLV_IE, tag=0x76):
+ pass
+
+# TS 101 220 Table 7.23
+class ImpuList(BER_TLV_IE, tag=0x77):
+ pass
+
+# TS 101 220 Table 7.23
+class ImsStatusCode(BER_TLV_IE, tag=0x77):
+ pass
+
+# TS 101 220 Table 7.23
+class DateTimeAndTimezone(BER_TLV_IE, tag=0x26):
+ pass
+
+# TS 101 220 Table 7.23
+class PdpPdnPduType(BER_TLV_IE, tag=0x0B):
+ pass
+
+# TS 101 220 Table 7.23
+class GadShape(BER_TLV_IE, tag=0x77):
+ pass
+
+# TS 101 220 Table 7.23
+class NmeaSentence(BER_TLV_IE, tag=0x78):
+ pass
+
+# TS 101 220 Table 7.23
+class WlanAccessStatus(BER_TLV_IE, tag=0x4B):
+ pass
+
+# TS 101 220 Table 7.17
+class EventDownload(BER_TLV_IE, tag=0xD6,
+ nested=[EventList, DeviceIdentities,
+ # 7.5.1.2 (I-)WLAN Access Status
+ WlanAccessStatus,
+ # 7.5.1A.2 MT Call
+ TransactionIdentifier, Address,
+ Subaddress, ImsURI, MediaType, UriTruncated,
+ # 7.5.2.2 Network Rejection
+ LocationInformation, RoutingAreaIdentification, TrackingAreaIdentification,
+ AccessTechnology, UpdateAttachRegistrationType, RejectionCauseCode,
+ ExtendedRejectionCauseCode,
+ # 7.5.2A.2 Call Connected
+ # TransactionIdentifier, MediaType
+ # 7.5.3.2 CSG Cell Selection
+ # AccessTechnology
+ CsgCellSelectionStatus, CsgId, HnbName, PlmnId,
+ # 7.5.3A.2 CAll Disconnected
+ # TransactionIdentifier, MediaType,
+ ImsCallDisconnectionStatus,
+ # TS 102 223 7.5.4 LocationStatusEvent
+ # TS 102 223 7.5.5 UserActivityEvent
+ # TS 102 223 7.5.6 IdleScreenAvailableEvent
+ # TS 102 223 7.5.7 CardReaderStatusEvent
+ # TS 102 223 7.5.8 LanguageSelectionEvent
+ # TS 102 223 7.5.9 BrowserTerminationEvent
+ # TS 102 223 7.5.10 DataAvailableEvent
+ # TS 102 223 7.5.11 ChannelStatusEvent
+ # TS 102 223 7.5.12 AccessTechnologyChangeEvent
+ # TS 102 223 7.5.13 DisplayParametersChangedEvent
+ # TS 102 223 7.5.14 LocalConnectionEvent
+ # TS 102 223 7.5.15 NetworkSearchModeChangeEvent
+ # TS 102 223 7.5.16 BrowsingStatusEvent
+ # TS 102 223 7.5.17 FramesInformationChangedEvent
+ # 7.5.20 Incoming IMS Data
+ Iari,
+ # 7.5.21 MS Registration Event
+ ImpuList, ImsStatusCode,
+ # 7.5.24 / TS 102 223 7.5.22 PollIntervalNegotiation
+ # 7.5.25 DataConnectionStatusChangeEvent
+ DataConnectionStatus, DataConnectionType, SmCause,
+ # TransactionIdentifier, LocationInformation, AccessTechnology
+ DateTimeAndTimezone, LocationStatus, NetworkAccessName, PdpPdnPduType,
+ # 7.7 / TS 102 223 7.6 MMS Transfer Status
+ # 7.8 / TS 102 223 MMS Notification Download
+ # 7.9 / TS 102 223 8.8 Terminal Applications
+ ]):
+ pass
+
+# TS 101 220 Table 7.17
+class TimerExpiration(BER_TLV_IE, tag=0xD7):
+ pass
+
+# TS 101 220 Table 7.17 + TS 31.111 7.6.2
class USSDDownload(BER_TLV_IE, tag=0xD9,
nested=[DeviceIdentities, USSDString]):
pass
+# TS 101 220 Table 7.17 + TS 102 223 7.6
+class MmsTransferStatus(BER_TLV_IE, tag=0xDA):
+ pass
+
+# TS 101 220 Table 7.17 + 102 223
+class MmsNotificationDownload(BER_TLV_IE, tag=0xDB):
+ pass
+
+# TS 101 220 Table 7.17 + 102 223 7.8
+class TerminalApplication(BER_TLV_IE, tag=0xDC):
+ pass
+
+# TS 101 220 Table 7.17 + TS 31.111 7.10.2
+class GeographicalLocation(BER_TLV_IE, tag=0xDD,
+ nested=[DeviceIdentities, GadShape, NmeaSentence]):
+ pass
+# TS 101 220 Table 7.17
+class EnvelopeContainer(BER_TLV_IE, tag=0xDE):
+ pass
+# TS 101 220 Table 7.17
+class ProSeReport(BER_TLV_IE, tag=0xDF):
+ pass
+# TS 101 220 Table 7.17
class ProactiveCmd(BER_TLV_IE):
def _compute_tag(self) -> int:
return 0xD0
+class EventCollection(TLV_IE_Collection,
+ nested=[SMSPPDownload, SMSCBDownload,
+ EventDownload, CallControl, MoShortMessageControl,
+ USSDDownload, GeographicalLocation, ProSeReport]):
+ pass
+
+
# TS 101 220 Table 7.17 + 102 223 6.6.13/9.4 + TS 31.111 6.6.13
class Refresh(ProactiveCmd, tag=0x01,
nested=[CommandDetails, DeviceIdentities, FileList, Aid, AlphaIdentifier,
@@ -978,8 +1152,8 @@ class ProactiveCommandBase(BER_TLV_IE, tag=0xD0, nested=[CommandDetails]):
for c in self.children:
if type(c).__name__ == 'CommandDetails':
return c
- else:
- return None
+ else:
+ return None
class ProactiveCommand(TLV_IE_Collection,
nested=[Refresh, MoreTime, PollInterval, PollingOff, SetUpEventList, SetUpCall,
@@ -997,7 +1171,7 @@ class ProactiveCommand(TLV_IE_Collection,
more difficult than any normal TLV IE Collection, because the content of one of the IEs defines the
definitions of all the other IEs. So we first need to find the CommandDetails, and then parse according
to the command type indicated in that IE data."""
- def from_bytes(self, binary: bytes) -> List[TLV_IE]:
+ def from_bytes(self, binary: bytes, context: dict = {}) -> List[TLV_IE]:
# do a first parse step to get the CommandDetails
pcmd = ProactiveCommandBase()
pcmd.from_tlv(binary)
@@ -1007,7 +1181,7 @@ class ProactiveCommand(TLV_IE_Collection,
if cmd_type in self.members_by_tag:
cls = self.members_by_tag[cmd_type]
inst = cls()
- dec, remainder = inst.from_tlv(binary)
+ _dec, remainder = inst.from_tlv(binary)
self.decoded = inst
else:
self.decoded = pcmd
@@ -1019,9 +1193,16 @@ class ProactiveCommand(TLV_IE_Collection,
def to_dict(self):
return self.decoded.to_dict()
- def to_bytes(self):
+ def to_bytes(self, context: dict = {}):
return self.decoded.to_tlv()
+# TS 101 223 Section 6.8.0
+class TerminalResponse(TLV_IE_Collection,
+ nested=[CommandDetails, DeviceIdentities, Result,
+ Duration, TextString, ItemIdentifier,
+ #TODO: LocalInformation and other optional/conditional IEs
+ ]):
+ pass
# reasonable default for playing with OTA
# 010203040506070809101112131415161718192021222324252627282930313233
diff --git a/pySim/cdma_ruim.py b/pySim/cdma_ruim.py
index 3fab558..a711300 100644
--- a/pySim/cdma_ruim.py
+++ b/pySim/cdma_ruim.py
@@ -19,15 +19,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import enum
+from construct import Bytewise, BitStruct, BitsInteger, Struct, FlagsEnum
+
from pySim.utils import *
from pySim.filesystem import *
from pySim.profile import match_ruim
-from pySim.profile import CardProfile
+from pySim.profile import CardProfile, CardProfileAddon
from pySim.ts_51_011 import CardProfileSIM
from pySim.ts_51_011 import DF_TELECOM, DF_GSM
from pySim.ts_51_011 import EF_ServiceTable
from pySim.construct import *
-from construct import *
# Mapping between CDMA Service Number and its description
@@ -91,7 +92,7 @@ class EF_SPN(TransparentEF):
_test_de_encode = [
( "010801536b796c696e6b204e57ffffffffffffffffffffffffffffffffffffffffffff",
- { 'rfu0' : 0, 'show_in_hsa' : True, 'rfu2' : 0,
+ { 'rfu1' : 0, 'show_in_hsa' : True, 'rfu2' : 0,
'char_encoding' : 8, 'lang_ind' : 1, 'spn' : 'Skylink NW' } ),
]
@@ -115,8 +116,9 @@ class EF_AD(TransparentEF):
'''3.4.33 Administrative Data'''
_test_de_encode = [
- ( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : '' } ),
+ ( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : b'' } ),
]
+ _test_no_pad = True
class OP_MODE(enum.IntEnum):
normal = 0x00
@@ -133,9 +135,9 @@ class EF_AD(TransparentEF):
# Byte 1: Display Condition
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
# Bytes 2-3: Additional information
- 'additional_info'/Bytes(2),
+ 'additional_info'/HexAdapter(Bytes(2)),
# Bytes 4..: RFU
- 'rfu'/GreedyBytesRFU,
+ 'rfu'/HexAdapter(GreedyBytesRFU),
)
@@ -184,10 +186,21 @@ class CardProfileRUIM(CardProfile):
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
@staticmethod
- def decode_select_response(resp_hex: str) -> object:
+ def decode_select_response(data_hex: str) -> object:
# TODO: Response parameters/data in case of DF_CDMA (section 2.6)
- return CardProfileSIM.decode_select_response(resp_hex)
+ return CardProfileSIM.decode_select_response(data_hex)
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_ruim(scc)
+
+class AddonRUIM(CardProfileAddon):
+ """An Addon that can be found on on a combined SIM + RUIM or UICC + RUIM to support CDMA."""
+ def __init__(self):
+ files = [
+ DF_CDMA()
+ ]
+ super().__init__('RUIM', desc='CDMA RUIM', files_in_mf=files)
+
+ def probe(self, card: 'CardBase') -> bool:
+ return card.file_exists(self.files_in_mf[0].fid)
diff --git a/pySim/commands.py b/pySim/commands.py
index 3c6ae49..c0ac1d7 100644
--- a/pySim/commands.py
+++ b/pySim/commands.py
@@ -5,7 +5,7 @@
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
-# Copyright (C) 2010-2021 Harald Welte <laforge@gnumonks.org>
+# Copyright (C) 2010-2024 Harald Welte <laforge@gnumonks.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,20 +21,178 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-from construct import *
-from pySim.construct import LV
-from pySim.utils import rpad, b2h, h2b, sw_match, bertlv_encode_len, Hexstr, h2i, str_sanitize, expand_hex
+from typing import List, Tuple
+import typing # construct also has a Union, so we do typing.Union below
+
+from construct import Construct, Struct, Const, Select
+from construct import Optional as COptional
+from pySim.construct import LV, filter_dict
+from pySim.utils import rpad, lpad, b2h, h2b, sw_match, bertlv_encode_len, h2i, i2h, str_sanitize, expand_hex, SwMatchstr
+from pySim.utils import Hexstr, SwHexstr, ResTuple
from pySim.exceptions import SwMatchError
+from pySim.transport import LinkBase
+
+# A path can be either just a FID or a list of FID
+Path = typing.Union[Hexstr, List[Hexstr]]
+
+def lchan_nr_to_cla(cla: int, lchan_nr: int) -> int:
+ """Embed a logical channel number into the CLA byte."""
+ # TS 102 221 10.1.1 Coding of Class Byte
+ if lchan_nr < 4:
+ # standard logical channel number
+ if cla >> 4 in [0x0, 0xA, 0x8]:
+ return (cla & 0xFC) | (lchan_nr & 3)
+ else:
+ raise ValueError('Undefined how to use CLA %2X with logical channel %u' % (cla, lchan_nr))
+ elif lchan_nr < 16:
+ # extended logical channel number
+ if cla >> 6 in [1, 3]:
+ return (cla & 0xF0) | ((lchan_nr - 4) & 0x0F)
+ else:
+ raise ValueError('Undefined how to use CLA %2X with logical channel %u' % (cla, lchan_nr))
+ else:
+ raise ValueError('logical channel outside of range 0 .. 15')
+def cla_with_lchan(cla_byte: Hexstr, lchan_nr: int) -> Hexstr:
+ """Embed a logical channel number into the hex-string encoded CLA value."""
+ cla_int = h2i(cla_byte)[0]
+ return i2h([lchan_nr_to_cla(cla_int, lchan_nr)])
class SimCardCommands:
- def __init__(self, transport):
+ """Class providing methods for various card-specific commands such as SELECT, READ BINARY, etc.
+ Historically one instance exists below CardBase, but with the introduction of multiple logical
+ channels there can be multiple instances. The lchan number will then be patched into the CLA
+ byte by the respective instance. """
+ def __init__(self, transport: LinkBase, lchan_nr: int = 0):
self._tp = transport
- self.cla_byte = "a0"
+ self._cla_byte = None
self.sel_ctrl = "0000"
+ self.lchan_nr = lchan_nr
+ # invokes the setter below
+ self.cla_byte = "a0"
+ self.scp = None # Secure Channel Protocol
+
+ def fork_lchan(self, lchan_nr: int) -> 'SimCardCommands':
+ """Fork a per-lchan specific SimCardCommands instance off the current instance."""
+ ret = SimCardCommands(transport = self._tp, lchan_nr = lchan_nr)
+ ret.cla_byte = self._cla_byte
+ ret.sel_ctrl = self.sel_ctrl
+ return ret
+
+ @property
+ def cla_byte(self) -> Hexstr:
+ """Return the (cached) patched default CLA byte for this card."""
+ return self._cla4lchan
+
+ @property
+ def max_cmd_len(self) -> int:
+ """Maximum length of the command apdu data section. Depends on secure channel protocol used."""
+ if self.scp:
+ return 255 - self.scp.overhead
+ else:
+ return 255
+
+ @cla_byte.setter
+ def cla_byte(self, new_val: Hexstr):
+ """Set the (raw, without lchan) default CLA value for this card."""
+ self._cla_byte = new_val
+ # compute cached result
+ self._cla4lchan = cla_with_lchan(self._cla_byte, self.lchan_nr)
+
+ def cla4lchan(self, cla: Hexstr) -> Hexstr:
+ """Compute the lchan-patched value of the given CLA value. If no CLA
+ value is provided as argument, the lchan-patched version of the SimCardCommands._cla_byte
+ value is used. Most commands will use the latter, while some wish to override it and
+ can pass it as argument here."""
+ if not cla:
+ # return cached result to avoid re-computing this over and over again
+ return self._cla4lchan
+ else:
+ return cla_with_lchan(cla, self.lchan_nr)
+
+ def send_apdu(self, pdu: Hexstr) -> ResTuple:
+ """Sends an APDU and auto fetch response data
+
+ Args:
+ pdu : string of hexadecimal characters (ex. "A0A40000023F00")
+ Returns:
+ tuple(data, sw), where
+ data : string (in hex) of returned data (ex. "074F4EFFFF")
+ sw : string (in hex) of status word (ex. "9000")
+ """
+ if self.scp:
+ return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
+ else:
+ return self._tp.send_apdu(pdu)
+
+ def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
+ """Sends an APDU and check returned SW
+
+ Args:
+ pdu : string of hexadecimal characters (ex. "A0A40000023F00")
+ sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
+ digits using a '?' to add some ambiguity if needed.
+ Returns:
+ tuple(data, sw), where
+ data : string (in hex) of returned data (ex. "074F4EFFFF")
+ sw : string (in hex) of status word (ex. "9000")
+ """
+ if self.scp:
+ return self.scp.send_apdu_wrapper(self._tp.send_apdu_checksw, pdu, sw)
+ else:
+ return self._tp.send_apdu_checksw(pdu, sw)
+
+ def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr, cmd_constr: Construct,
+ cmd_data: Hexstr, resp_constr: Construct) -> Tuple[dict, SwHexstr]:
+ """Build and sends an APDU using a 'construct' definition; parses response.
+
+ Args:
+ cla : string (in hex) ISO 7816 class byte
+ ins : string (in hex) ISO 7816 instruction byte
+ p1 : string (in hex) ISO 7116 Parameter 1 byte
+ p2 : string (in hex) ISO 7116 Parameter 2 byte
+ cmd_cosntr : defining how to generate binary APDU command data
+ cmd_data : command data passed to cmd_constr
+ resp_cosntr : defining how to decode binary APDU response data
+ Returns:
+ Tuple of (decoded_data, sw)
+ """
+ cmd = cmd_constr.build(cmd_data) if cmd_data else ''
+ p3 = i2h([len(cmd)])
+ pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)])
+ (data, sw) = self.send_apdu(pdu)
+ if data:
+ # filter the resulting dict to avoid '_io' members inside
+ rsp = filter_dict(resp_constr.parse(h2b(data)))
+ else:
+ rsp = None
+ return (rsp, sw)
+
+ def send_apdu_constr_checksw(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr,
+ cmd_constr: Construct, cmd_data: Hexstr, resp_constr: Construct,
+ sw_exp: SwMatchstr="9000") -> Tuple[dict, SwHexstr]:
+ """Build and sends an APDU using a 'construct' definition; parses response.
+
+ Args:
+ cla : string (in hex) ISO 7816 class byte
+ ins : string (in hex) ISO 7816 instruction byte
+ p1 : string (in hex) ISO 7116 Parameter 1 byte
+ p2 : string (in hex) ISO 7116 Parameter 2 byte
+ cmd_cosntr : defining how to generate binary APDU command data
+ cmd_data : command data passed to cmd_constr
+ resp_cosntr : defining how to decode binary APDU response data
+ exp_sw : string (in hex) of status word (ex. "9000")
+ Returns:
+ Tuple of (decoded_data, sw)
+ """
+ (rsp, sw) = self.send_apdu_constr(cla, ins,
+ p1, p2, cmd_constr, cmd_data, resp_constr)
+ if not sw_match(sw, sw_exp):
+ raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
+ return (rsp, sw)
# Extract a single FCP item from TLV
- def __parse_fcp(self, fcp):
+ def __parse_fcp(self, fcp: Hexstr):
# see also: ETSI TS 102 221, chapter 11.1.1.3.1 Response for MF,
# DF or ADF
from pytlv.TLV import TLV
@@ -53,6 +211,7 @@ class SimCardCommands:
# checking if the length of the remaining TLV string matches
# what we get in the length field.
# See also ETSI TS 102 221, chapter 11.1.1.3.0 Base coding.
+ # TODO: this likely just is normal BER-TLV ("All data objects are BER-TLV except if otherwise # defined.")
exp_tlv_len = int(fcp[2:4], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 4
@@ -60,6 +219,7 @@ class SimCardCommands:
exp_tlv_len = int(fcp[2:6], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 6
+ raise ValueError('Cannot determine length of TLV-length')
# Skip FCP tag and length
tlv = fcp[skip:]
@@ -88,11 +248,11 @@ class SimCardCommands:
else:
return int(r[-1][4:8], 16)
- def get_atr(self) -> str:
+ def get_atr(self) -> Hexstr:
"""Return the ATR of the currently inserted card."""
return self._tp.get_atr()
- def try_select_path(self, dir_list):
+ def try_select_path(self, dir_list: List[Hexstr]) -> List[ResTuple]:
""" Try to select a specified path
Args:
@@ -100,17 +260,16 @@ class SimCardCommands:
"""
rv = []
- if type(dir_list) is not list:
+ if not isinstance(dir_list, list):
dir_list = [dir_list]
for i in dir_list:
- data, sw = self._tp.send_apdu(
- self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
+ data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
rv.append((data, sw))
if sw != '9000':
return rv
return rv
- def select_path(self, dir_list):
+ def select_path(self, dir_list: Path) -> List[Hexstr]:
"""Execute SELECT for an entire list/path of FIDs.
Args:
@@ -120,27 +279,27 @@ class SimCardCommands:
list of return values (FCP in hex encoding) for each element of the path
"""
rv = []
- if type(dir_list) is not list:
+ if not isinstance(dir_list, list):
dir_list = [dir_list]
for i in dir_list:
- data, sw = self.select_file(i)
+ data, _sw = self.select_file(i)
rv.append(data)
return rv
- def select_file(self, fid: str):
+ def select_file(self, fid: Hexstr) -> ResTuple:
"""Execute SELECT a given file by FID.
Args:
fid : file identifier as hex string
"""
- return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
+ return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
- def select_parent_df(self):
+ def select_parent_df(self) -> ResTuple:
"""Execute SELECT to switch to the parent DF """
- return self._tp.send_apdu_checksw(self.cla_byte + "a4030400")
+ return self.send_apdu_checksw(self.cla_byte + "a4030400")
- def select_adf(self, aid: str):
+ def select_adf(self, aid: Hexstr) -> ResTuple:
"""Execute SELECT a given Applicaiton ADF.
Args:
@@ -148,9 +307,9 @@ class SimCardCommands:
"""
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
- return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
+ return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
- def read_binary(self, ef, length: int = None, offset: int = 0):
+ def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
"""Execute READD BINARY.
Args:
@@ -169,19 +328,33 @@ class SimCardCommands:
total_data = ''
chunk_offset = 0
while chunk_offset < length:
- chunk_len = min(255, length-chunk_offset)
+ chunk_len = min(self.max_cmd_len, length-chunk_offset)
pdu = self.cla_byte + \
'b0%04x%02x' % (offset + chunk_offset, chunk_len)
try:
- data, sw = self._tp.send_apdu_checksw(pdu)
+ data, sw = self.send_apdu_checksw(pdu)
except Exception as e:
raise ValueError('%s, failed to read (offset %d)' %
- (str_sanitize(str(e)), offset))
+ (str_sanitize(str(e)), offset)) from e
total_data += data
chunk_offset += chunk_len
return total_data, sw
- def update_binary(self, ef, data: str, offset: int = 0, verify: bool = False, conserve: bool = False):
+ def __verify_binary(self, ef, data: str, offset: int = 0):
+ """Verify contents of transparent EF.
+
+ Args:
+ ef : string or list of strings indicating name or path of transparent EF
+ data : hex string of expected data
+ offset : byte offset in file from which to start verifying
+ """
+ res = self.read_binary(ef, len(data) // 2, offset)
+ if res[0].lower() != data.lower():
+ raise ValueError('Binary verification failed (expected %s, got %s)' % (
+ data.lower(), res[0].lower()))
+
+ def update_binary(self, ef: Path, data: Hexstr, offset: int = 0, verify: bool = False,
+ conserve: bool = False) -> ResTuple:
"""Execute UPDATE BINARY.
Args:
@@ -198,44 +371,38 @@ class SimCardCommands:
# Save write cycles by reading+comparing before write
if conserve:
- data_current, sw = self.read_binary(ef, data_length, offset)
- if data_current == data:
- return None, sw
+ try:
+ data_current, sw = self.read_binary(ef, data_length, offset)
+ if data_current == data:
+ return None, sw
+ except Exception:
+ # cannot read data. This is not a fatal error, as reading is just done to
+ # conserve the amount of smart card writes. The access conditions of the file
+ # may well permit us to UPDATE but not permit us to READ. So let's ignore
+ # any such exception during READ.
+ pass
self.select_path(ef)
total_data = ''
chunk_offset = 0
while chunk_offset < data_length:
- chunk_len = min(255, data_length - chunk_offset)
+ chunk_len = min(self.max_cmd_len, data_length - chunk_offset)
# chunk_offset is bytes, but data slicing is hex chars, so we need to multiply by 2
pdu = self.cla_byte + \
'd6%04x%02x' % (offset + chunk_offset, chunk_len) + \
data[chunk_offset*2: (chunk_offset+chunk_len)*2]
try:
- chunk_data, chunk_sw = self._tp.send_apdu_checksw(pdu)
+ chunk_data, chunk_sw = self.send_apdu_checksw(pdu)
except Exception as e:
raise ValueError('%s, failed to write chunk (chunk_offset %d, chunk_len %d)' %
- (str_sanitize(str(e)), chunk_offset, chunk_len))
+ (str_sanitize(str(e)), chunk_offset, chunk_len)) from e
total_data += data
chunk_offset += chunk_len
if verify:
- self.verify_binary(ef, data, offset)
+ self.__verify_binary(ef, data, offset)
return total_data, chunk_sw
- def verify_binary(self, ef, data: str, offset: int = 0):
- """Verify contents of transparent EF.
-
- Args:
- ef : string or list of strings indicating name or path of transparent EF
- data : hex string of expected data
- offset : byte offset in file from which to start verifying
- """
- res = self.read_binary(ef, len(data) // 2, offset)
- if res[0].lower() != data.lower():
- raise ValueError('Binary verification failed (expected %s, got %s)' % (
- data.lower(), res[0].lower()))
-
- def read_record(self, ef, rec_no: int):
+ def read_record(self, ef: Path, rec_no: int) -> ResTuple:
"""Execute READ RECORD.
Args:
@@ -245,10 +412,23 @@ class SimCardCommands:
r = self.select_path(ef)
rec_length = self.__record_len(r)
pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length)
- return self._tp.send_apdu_checksw(pdu)
+ return self.send_apdu_checksw(pdu)
+
+ def __verify_record(self, ef: Path, rec_no: int, data: str):
+ """Verify record against given data
- def update_record(self, ef, rec_no: int, data: str, force_len: bool = False, verify: bool = False,
- conserve: bool = False):
+ Args:
+ ef : string or list of strings indicating name or path of linear fixed EF
+ rec_no : record number to read
+ data : hex string of data to be verified
+ """
+ res = self.read_record(ef, rec_no)
+ if res[0].lower() != data.lower():
+ raise ValueError('Record verification failed (expected %s, got %s)' % (
+ data.lower(), res[0].lower()))
+
+ def update_record(self, ef: Path, rec_no: int, data: Hexstr, force_len: bool = False,
+ verify: bool = False, conserve: bool = False, leftpad: bool = False) -> ResTuple:
"""Execute UPDATE RECORD.
Args:
@@ -258,6 +438,7 @@ class SimCardCommands:
force_len : enforce record length by using the actual data length
verify : verify data by re-reading the record
conserve : read record and compare it with data, skip write on match
+ leftpad : apply 0xff padding from the left instead from the right side.
"""
res = self.select_path(ef)
@@ -270,39 +451,36 @@ class SimCardCommands:
else:
# make sure the input data is padded to the record length using 0xFF.
# In cases where the input data exceed we throw an exception.
- if (len(data) // 2 > rec_length):
+ if len(data) // 2 > rec_length:
raise ValueError('Data length exceeds record length (expected max %d, got %d)' % (
rec_length, len(data) // 2))
- elif (len(data) // 2 < rec_length):
- data = rpad(data, rec_length * 2)
+ elif len(data) // 2 < rec_length:
+ if leftpad:
+ data = lpad(data, rec_length * 2)
+ else:
+ data = rpad(data, rec_length * 2)
# Save write cycles by reading+comparing before write
if conserve:
- data_current, sw = self.read_record(ef, rec_no)
- data_current = data_current[0:rec_length*2]
- if data_current == data:
- return None, sw
+ try:
+ data_current, sw = self.read_record(ef, rec_no)
+ data_current = data_current[0:rec_length*2]
+ if data_current == data:
+ return None, sw
+ except Exception:
+ # cannot read data. This is not a fatal error, as reading is just done to
+ # conserve the amount of smart card writes. The access conditions of the file
+ # may well permit us to UPDATE but not permit us to READ. So let's ignore
+ # any such exception during READ.
+ pass
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
- res = self._tp.send_apdu_checksw(pdu)
+ res = self.send_apdu_checksw(pdu)
if verify:
- self.verify_record(ef, rec_no, data)
+ self.__verify_record(ef, rec_no, data)
return res
- def verify_record(self, ef, rec_no: int, data: str):
- """Verify record against given data
-
- Args:
- ef : string or list of strings indicating name or path of linear fixed EF
- rec_no : record number to read
- data : hex string of data to be verified
- """
- res = self.read_record(ef, rec_no)
- if res[0].lower() != data.lower():
- raise ValueError('Record verification failed (expected %s, got %s)' % (
- data.lower(), res[0].lower()))
-
- def record_size(self, ef):
+ def record_size(self, ef: Path) -> int:
"""Determine the record size of given file.
Args:
@@ -311,7 +489,7 @@ class SimCardCommands:
r = self.select_path(ef)
return self.__record_len(r)
- def record_count(self, ef):
+ def record_count(self, ef: Path) -> int:
"""Determine the number of records in given file.
Args:
@@ -320,7 +498,7 @@ class SimCardCommands:
r = self.select_path(ef)
return self.__len(r) // self.__record_len(r)
- def binary_size(self, ef):
+ def binary_size(self, ef: Path) -> int:
"""Determine the size of given transparent file.
Args:
@@ -330,14 +508,14 @@ class SimCardCommands:
return self.__len(r)
# TS 102 221 Section 11.3.1 low-level helper
- def _retrieve_data(self, tag: int, first: bool = True):
+ def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
if first:
- pdu = '80cb008001%02x' % (tag)
+ pdu = self.cla4lchan('80') + 'cb008001%02x' % (tag)
else:
- pdu = '80cb000000'
- return self._tp.send_apdu_checksw(pdu)
+ pdu = self.cla4lchan('80') + 'cb000000'
+ return self.send_apdu_checksw(pdu)
- def retrieve_data(self, ef, tag: int):
+ def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
"""Execute RETRIEVE DATA, see also TS 102 221 Section 11.3.1.
Args
@@ -351,23 +529,23 @@ class SimCardCommands:
# retrieve first block
data, sw = self._retrieve_data(tag, first=True)
total_data += data
- while sw == '62f1' or sw == '62f2':
+ while sw in ['62f1', '62f2']:
data, sw = self._retrieve_data(tag, first=False)
total_data += data
return total_data, sw
# TS 102 221 Section 11.3.2 low-level helper
- def _set_data(self, data: str, first: bool = True):
+ def _set_data(self, data: Hexstr, first: bool = True) -> ResTuple:
if first:
p1 = 0x80
else:
p1 = 0x00
- if isinstance(data, bytes) or isinstance(data, bytearray):
+ if isinstance(data, (bytes, bytearray)):
data = b2h(data)
- pdu = '80db00%02x%02x%s' % (p1, len(data)//2, data)
- return self._tp.send_apdu_checksw(pdu)
+ pdu = self.cla4lchan('80') + 'db00%02x%02x%s' % (p1, len(data)//2, data)
+ return self.send_apdu_checksw(pdu)
- def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False):
+ def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
"""Execute SET DATA.
Args
@@ -392,13 +570,13 @@ class SimCardCommands:
total_len = len(tlv_bin)
remaining = tlv_bin
while len(remaining) > 0:
- fragment = remaining[:255]
+ fragment = remaining[:self.max_cmd_len]
rdata, sw = self._set_data(fragment, first=first)
first = False
- remaining = remaining[255:]
+ remaining = remaining[self.max_cmd_len:]
return rdata, sw
- def run_gsm(self, rand: str):
+ def run_gsm(self, rand: Hexstr) -> ResTuple:
"""Execute RUN GSM ALGORITHM.
Args:
@@ -407,9 +585,9 @@ class SimCardCommands:
if len(rand) != 32:
raise ValueError('Invalid rand')
self.select_path(['3f00', '7f20'])
- return self._tp.send_apdu_checksw('a0' + '88000010' + rand, sw='9000')
+ return self.send_apdu_checksw(self.cla4lchan('a0') + '88000010' + rand, sw='9000')
- def authenticate(self, rand: str, autn: str, context='3g'):
+ def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
"""Execute AUTHENTICATE (USIM/ISIM).
Args:
@@ -418,10 +596,9 @@ class SimCardCommands:
context : 16 byte random data ('3g' or 'gsm')
"""
# 3GPP TS 31.102 Section 7.1.2.1
- AuthCmd3G = Struct('rand'/LV, 'autn'/Optional(LV))
+ AuthCmd3G = Struct('rand'/LV, 'autn'/COptional(LV))
AuthResp3GSyncFail = Struct(Const(b'\xDC'), 'auts'/LV)
- AuthResp3GSuccess = Struct(
- Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/Optional(LV))
+ AuthResp3GSuccess = Struct(Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/COptional(LV))
AuthResp3G = Select(AuthResp3GSyncFail, AuthResp3GSuccess)
# build parameters
cmd_data = {'rand': rand, 'autn': autn}
@@ -429,7 +606,9 @@ class SimCardCommands:
p2 = '81'
elif context == 'gsm':
p2 = '80'
- (data, sw) = self._tp.send_apdu_constr_checksw(
+ else:
+ raise ValueError("Unsupported context '%s'" % context)
+ (data, sw) = self.send_apdu_constr_checksw(
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
if 'auts' in data:
ret = {'synchronisation_failure': data}
@@ -437,47 +616,47 @@ class SimCardCommands:
ret = {'successful_3g_authentication': data}
return (ret, sw)
- def status(self):
+ def status(self) -> ResTuple:
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
- return self._tp.send_apdu_checksw('80F20000ff')
+ return self.send_apdu_checksw(self.cla4lchan('80') + 'F2000000')
- def deactivate_file(self):
+ def deactivate_file(self) -> ResTuple:
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
- return self._tp.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
+ return self.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
- def activate_file(self, fid):
+ def activate_file(self, fid: Hexstr) -> ResTuple:
"""Execute ACTIVATE FILE command as per TS 102 221 Section 11.1.15.
Args:
fid : file identifier as hex string
"""
- return self._tp.send_apdu_checksw(self.cla_byte + '44000002' + fid)
+ return self.send_apdu_checksw(self.cla_byte + '44000002' + fid)
- def create_file(self, payload: Hexstr):
+ def create_file(self, payload: Hexstr) -> ResTuple:
"""Execute CREEATE FILE command as per TS 102 222 Section 6.3"""
- return self._tp.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
+ return self.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
- def resize_file(self, payload: Hexstr):
+ def resize_file(self, payload: Hexstr) -> ResTuple:
"""Execute RESIZE FILE command as per TS 102 222 Section 6.10"""
- return self._tp.send_apdu_checksw('80d40000%02x%s' % (len(payload)//2, payload))
+ return self.send_apdu_checksw(self.cla4lchan('80') + 'd40000%02x%s' % (len(payload)//2, payload))
- def delete_file(self, fid):
+ def delete_file(self, fid: Hexstr) -> ResTuple:
"""Execute DELETE FILE command as per TS 102 222 Section 6.4"""
- return self._tp.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
+ return self.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
- def terminate_df(self, fid):
+ def terminate_df(self, fid: Hexstr) -> ResTuple:
"""Execute TERMINATE DF command as per TS 102 222 Section 6.7"""
- return self._tp.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
+ return self.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
- def terminate_ef(self, fid):
+ def terminate_ef(self, fid: Hexstr) -> ResTuple:
"""Execute TERMINATE EF command as per TS 102 222 Section 6.8"""
- return self._tp.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
+ return self.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
- def terminate_card_usage(self):
+ def terminate_card_usage(self) -> ResTuple:
"""Execute TERMINATE CARD USAGE command as per TS 102 222 Section 6.9"""
- return self._tp.send_apdu_checksw(self.cla_byte + 'fe000000')
+ return self.send_apdu_checksw(self.cla_byte + 'fe000000')
- def manage_channel(self, mode='open', lchan_nr=0):
+ def manage_channel(self, mode: str = 'open', lchan_nr: int =0) -> ResTuple:
"""Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17.
Args:
@@ -489,20 +668,20 @@ class SimCardCommands:
else:
p1 = 0x00
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
- return self._tp.send_apdu_checksw(pdu)
+ return self.send_apdu_checksw(pdu)
- def reset_card(self):
+ def reset_card(self) -> Hexstr:
"""Physically reset the card"""
return self._tp.reset_card()
- def _chv_process_sw(self, op_name, chv_no, pin_code, sw):
+ def _chv_process_sw(self, op_name: str, chv_no: int, pin_code: Hexstr, sw: SwHexstr):
if sw_match(sw, '63cx'):
raise RuntimeError('Failed to %s chv_no 0x%02X with code 0x%s, %i tries left.' %
(op_name, chv_no, b2h(pin_code).upper(), int(sw[3])))
- elif (sw != '9000'):
+ if sw != '9000':
raise SwMatchError(sw, '9000')
- def verify_chv(self, chv_no: int, code: str):
+ def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
"""Verify a given CHV (Card Holder Verification == PIN)
Args:
@@ -510,8 +689,7 @@ class SimCardCommands:
code : chv code as hex string
"""
fc = rpad(b2h(code), 16)
- data, sw = self._tp.send_apdu(
- self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
+ data, sw = self.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('verify', chv_no, code, sw)
return (data, sw)
@@ -524,12 +702,11 @@ class SimCardCommands:
pin_code : new chv code as hex string
"""
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
- data, sw = self._tp.send_apdu(
- self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
+ data, sw = self.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
self._chv_process_sw('unblock', chv_no, pin_code, sw)
return (data, sw)
- def change_chv(self, chv_no: int, pin_code: str, new_pin_code: str):
+ def change_chv(self, chv_no: int, pin_code: Hexstr, new_pin_code: Hexstr) -> ResTuple:
"""Change a given CHV (Card Holder Verification == PIN)
Args:
@@ -538,12 +715,11 @@ class SimCardCommands:
new_pin_code : new chv code as hex string
"""
fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16)
- data, sw = self._tp.send_apdu(
- self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
+ data, sw = self.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
self._chv_process_sw('change', chv_no, pin_code, sw)
return (data, sw)
- def disable_chv(self, chv_no: int, pin_code: str):
+ def disable_chv(self, chv_no: int, pin_code: Hexstr) -> ResTuple:
"""Disable a given CHV (Card Holder Verification == PIN)
Args:
@@ -552,12 +728,11 @@ class SimCardCommands:
new_pin_code : new chv code as hex string
"""
fc = rpad(b2h(pin_code), 16)
- data, sw = self._tp.send_apdu(
- self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
+ data, sw = self.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('disable', chv_no, pin_code, sw)
return (data, sw)
- def enable_chv(self, chv_no: int, pin_code: str):
+ def enable_chv(self, chv_no: int, pin_code: Hexstr) -> ResTuple:
"""Enable a given CHV (Card Holder Verification == PIN)
Args:
@@ -565,31 +740,30 @@ class SimCardCommands:
pin_code : chv code as hex string
"""
fc = rpad(b2h(pin_code), 16)
- data, sw = self._tp.send_apdu(
- self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
+ data, sw = self.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('enable', chv_no, pin_code, sw)
return (data, sw)
- def envelope(self, payload: str):
+ def envelope(self, payload: Hexstr) -> ResTuple:
"""Send one ENVELOPE command to the SIM
Args:
payload : payload as hex string
"""
- return self._tp.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
+ return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
- def terminal_profile(self, payload: str):
+ def terminal_profile(self, payload: Hexstr) -> ResTuple:
"""Send TERMINAL PROFILE to card
Args:
payload : payload as hex string
"""
data_length = len(payload) // 2
- data, sw = self._tp.send_apdu(('80100000%02x' % data_length) + payload)
+ data, sw = self.send_apdu(('80100000%02x' % data_length) + payload)
return (data, sw)
# ETSI TS 102 221 11.1.22
- def suspend_uicc(self, min_len_secs: int = 60, max_len_secs: int = 43200):
+ def suspend_uicc(self, min_len_secs: int = 60, max_len_secs: int = 43200) -> Tuple[int, Hexstr, SwHexstr]:
"""Send SUSPEND UICC to the card.
Args:
@@ -599,50 +773,48 @@ class SimCardCommands:
def encode_duration(secs: int) -> Hexstr:
if secs >= 10*24*60*60:
return '04%02x' % (secs // (10*24*60*60))
- elif secs >= 24*60*60:
+ if secs >= 24*60*60:
return '03%02x' % (secs // (24*60*60))
- elif secs >= 60*60:
+ if secs >= 60*60:
return '02%02x' % (secs // (60*60))
- elif secs >= 60:
+ if secs >= 60:
return '01%02x' % (secs // 60)
- else:
- return '00%02x' % secs
+ return '00%02x' % secs
def decode_duration(enc: Hexstr) -> int:
time_unit = enc[:2]
length = h2i(enc[2:4])[0]
if time_unit == '04':
return length * 10*24*60*60
- elif time_unit == '03':
+ if time_unit == '03':
return length * 24*60*60
- elif time_unit == '02':
+ if time_unit == '02':
return length * 60*60
- elif time_unit == '01':
+ if time_unit == '01':
return length * 60
- elif time_unit == '00':
+ if time_unit == '00':
return length
- else:
- raise ValueError('Time unit must be 0x00..0x04')
+ raise ValueError('Time unit must be 0x00..0x04')
min_dur_enc = encode_duration(min_len_secs)
max_dur_enc = encode_duration(max_len_secs)
- data, sw = self._tp.send_apdu_checksw(
- '8076000004' + min_dur_enc + max_dur_enc)
+ data, sw = self.send_apdu_checksw('8076000004' + min_dur_enc + max_dur_enc)
negotiated_duration_secs = decode_duration(data[:4])
resume_token = data[4:]
return (negotiated_duration_secs, resume_token, sw)
# ETSI TS 102 221 11.1.22
- def resume_uicc(self, token: str):
+ def resume_uicc(self, token: Hexstr) -> ResTuple:
"""Send SUSPEND UICC (resume) to the card."""
if len(h2b(token)) != 8:
raise ValueError("Token must be 8 bytes long")
- data, sw = self._tp.send_apdu_checksw('8076010008' + token)
+ data, sw = self.send_apdu_checksw('8076010008' + token)
+ return (data, sw)
def get_data(self, tag: int, cla: int = 0x00):
- data, sw = self._tp.send_apdu('%02xca%04x00' % (cla, tag))
+ data, sw = self.send_apdu('%02xca%04x00' % (cla, tag))
return (data, sw)
# TS 31.102 Section 7.5.2
- def get_identity(self, context: int):
- data, sw = self._tp.send_apdu_checksw('807800%02x00' % (context))
+ def get_identity(self, context: int) -> Tuple[Hexstr, SwHexstr]:
+ data, sw = self.send_apdu_checksw('807800%02x00' % (context))
return (data, sw)
diff --git a/pySim/construct.py b/pySim/construct.py
index ab44a63..881f1f2 100644
--- a/pySim/construct.py
+++ b/pySim/construct.py
@@ -1,13 +1,20 @@
+"""Utility code related to the integration of the 'construct' declarative parser."""
+
+import typing
+import codecs
+import ipaddress
+
+import gsm0338
+
from construct.lib.containers import Container, ListContainer
from construct.core import EnumIntegerString
-import typing
-from construct import *
-from construct.core import evaluate, BitwisableString
+from construct import Adapter, Prefixed, Int8ub, GreedyBytes, Default, Flag, Byte, Construct, Enum
+from construct import BitsInteger, BitStruct, Bytes, StreamError, stream_read_entire, stream_write
+from construct import SizeofError, IntegerError, swapbytes
+from construct.core import evaluate
from construct.lib import integertypes
-from pySim.utils import b2h, h2b, swap_nibbles
-import gsm0338
-"""Utility code related to the integration of the 'construct' declarative parser."""
+from pySim.utils import b2h, h2b, swap_nibbles
# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
#
@@ -34,6 +41,199 @@ class HexAdapter(Adapter):
def _encode(self, obj, context, path):
return h2b(obj)
+class Utf8Adapter(Adapter):
+ """convert a bytes() type that contains utf8 encoded text to human readable text."""
+
+ def _decode(self, obj, context, path):
+ # In case the string contains only 0xff bytes we interpret it as an empty string
+ if obj == b'\xff' * len(obj):
+ return ""
+ return codecs.decode(obj, "utf-8")
+
+ def _encode(self, obj, context, path):
+ return codecs.encode(obj, "utf-8")
+
+class GsmOrUcs2Adapter(Adapter):
+ """Try to encode into a GSM 03.38 string; if that fails, fall back to UCS-2 as described
+ in TS 102 221 Annex A."""
+ def _decode(self, obj, context, path):
+ # In case the string contains only 0xff bytes we interpret it as an empty string
+ if obj == b'\xff' * len(obj):
+ return ""
+ # one of the magic bytes of TS 102 221 Annex A
+ if obj[0] in [0x80, 0x81, 0x82]:
+ ad = Ucs2Adapter(GreedyBytes)
+ else:
+ ad = GsmString(GreedyBytes)
+ return ad._decode(obj, context, path)
+
+ def _encode(self, obj, context, path):
+ # first try GSM 03.38; then fall back to TS 102 221 Annex A UCS-2
+ try:
+ ad = GsmString(GreedyBytes)
+ return ad._encode(obj, context, path)
+ except:
+ ad = Ucs2Adapter(GreedyBytes)
+ return ad._encode(obj, context, path)
+
+class Ucs2Adapter(Adapter):
+ """convert a bytes() type that contains UCS2 encoded characters encoded as defined in TS 102 221
+ Annex A to normal python string representation (and back)."""
+ def _decode(self, obj, context, path):
+ # In case the string contains only 0xff bytes we interpret it as an empty string
+ if obj == b'\xff' * len(obj):
+ return ""
+ if obj[0] == 0x80:
+ # TS 102 221 Annex A Variant 1
+ return codecs.decode(obj[1:], 'utf_16_be')
+ elif obj[0] == 0x81:
+ # TS 102 221 Annex A Variant 2
+ out = ""
+ # second byte contains a value indicating the number of characters
+ num_of_chars = obj[1]
+ # the third byte contains an 8 bit number which defines bits 15 to 8 of a 16 bit base
+ # pointer, where bit 16 is set to zero, and bits 7 to 1 are also set to zero. These
+ # sixteen bits constitute a base pointer to a "half-page" in the UCS2 code space
+ base_ptr = obj[2] << 7
+ for ch in obj[3:3+num_of_chars]:
+ # if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a
+ # GSM Default Alphabet character, whereas if bit 8 of the byte is set to one, then
+ # the remaining seven bits are an offset value added to the 16 bit base pointer
+ # defined earlier, and the resultant 16 bit value is a UCS2 code point
+ if ch & 0x80:
+ codepoint = (ch & 0x7f) + base_ptr
+ out += codecs.decode(codepoint.to_bytes(2, byteorder='big'), 'utf_16_be')
+ else:
+ out += codecs.decode(bytes([ch]), 'gsm03.38')
+ return out
+ elif obj[0] == 0x82:
+ # TS 102 221 Annex A Variant 3
+ out = ""
+ # second byte contains a value indicating the number of characters
+ num_of_chars = obj[1]
+ # third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
+ # pointer to a half-page in the UCS2 code space, for use with some or all of the
+ # remaining bytes in the string
+ base_ptr = obj[2] << 8 | obj[3]
+ for ch in obj[4:4+num_of_chars]:
+ # if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a
+ # GSM Default Alphabet character, whereas if bit 8 of the byte is set to one, the
+ # remaining seven bits are an offset value added to the base pointer defined in
+ # bytes three and four, and the resultant 16 bit value is a UCS2 code point, else: #
+ # GSM default alphabet
+ if ch & 0x80:
+ codepoint = (ch & 0x7f) + base_ptr
+ out += codecs.decode(codepoint.to_bytes(2, byteorder='big'), 'utf_16_be')
+ else:
+ out += codecs.decode(bytes([ch]), 'gsm03.38')
+ return out
+ else:
+ raise ValueError('First byte of TS 102 221 UCS-2 must be 0x80, 0x81 or 0x82')
+
+ def _encode(self, obj, context, path):
+ def encodable_in_gsm338(instr: str) -> bool:
+ """Determine if given input string is encode-ale in gsm03.38."""
+ try:
+ # TODO: figure out if/how we can constrain to default alphabet. The gsm0338
+ # library seems to include the spanish lock/shift table
+ codecs.encode(instr, 'gsm03.38')
+ except ValueError:
+ return False
+ return True
+
+ def codepoints_not_in_gsm338(instr: str) -> typing.List[int]:
+ """Return an integer list of UCS2 codepoints for all characters of 'inster'
+ which are not representable in the GSM 03.38 default alphabet."""
+ codepoint_list = []
+ for c in instr:
+ if encodable_in_gsm338(c):
+ continue
+ c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
+ codepoint_list.append(c_codepoint)
+ return codepoint_list
+
+ def diff_between_min_and_max_of_list(inlst: typing.List) -> int:
+ return max(inlst) - min(inlst)
+
+ def encodable_in_variant2(instr: str) -> bool:
+ codepoint_prefix = None
+ for c in instr:
+ if encodable_in_gsm338(c):
+ continue
+ c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
+ if c_codepoint >= 0x8000:
+ return False
+ c_prefix = c_codepoint >> 7
+ if codepoint_prefix is None:
+ codepoint_prefix = c_prefix
+ else:
+ if c_prefix != codepoint_prefix:
+ return False
+ return True
+
+ def encodable_in_variant3(instr: str) -> bool:
+ codepoint_list = codepoints_not_in_gsm338(instr)
+ # compute delta between max and min; check if it's encodable in 7 bits
+ if diff_between_min_and_max_of_list(codepoint_list) >= 0x80:
+ return False
+ return True
+
+ def _encode_variant1(instr: str) -> bytes:
+ """Encode according to TS 102 221 Annex A Variant 1"""
+ return b'\x80' + codecs.encode(instr, 'utf_16_be')
+
+ def _encode_variant2(instr: str) -> bytes:
+ """Encode according to TS 102 221 Annex A Variant 2"""
+ codepoint_prefix = None
+ # second byte contains a value indicating the number of characters
+ hdr = b'\x81' + len(instr).to_bytes(1, byteorder='big')
+ chars = b''
+ for c in instr:
+ try:
+ enc = codecs.encode(c, 'gsm03.38')
+ except ValueError:
+ c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
+ c_prefix = c_codepoint >> 7
+ if codepoint_prefix is None:
+ codepoint_prefix = c_prefix
+ assert codepoint_prefix == c_prefix
+ enc = (0x80 + (c_codepoint & 0x7f)).to_bytes(1, byteorder='big')
+ chars += enc
+ if codepoint_prefix is None:
+ codepoint_prefix = 0
+ return hdr + codepoint_prefix.to_bytes(1, byteorder='big') + chars
+
+ def _encode_variant3(instr: str) -> bytes:
+ """Encode according to TS 102 221 Annex A Variant 3"""
+ # second byte contains a value indicating the number of characters
+ hdr = b'\x82' + len(instr).to_bytes(1, byteorder='big')
+ chars = b''
+ codepoint_list = codepoints_not_in_gsm338(instr)
+ codepoint_base = min(codepoint_list)
+ for c in instr:
+ try:
+ # if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a GSM
+ # Default # Alphabet character
+ enc = codecs.encode(c, 'gsm03.38')
+ except ValueError:
+ # if bit 8 of the byte is set to one, the remaining seven bits are an offset
+ # value added to the base pointer defined in bytes three and four, and the
+ # resultant 16 bit value is a UCS2 code point
+ c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
+ c_codepoint_delta = c_codepoint - codepoint_base
+ assert c_codepoint_delta < 0x80
+ enc = (0x80 + c_codepoint_delta).to_bytes(1, byteorder='big')
+ chars += enc
+ # third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
+ # pointer to a half-page in the UCS2 code space
+ return hdr + codepoint_base.to_bytes(2, byteorder='big') + chars
+
+ if encodable_in_variant2(obj):
+ return _encode_variant2(obj)
+ elif encodable_in_variant3(obj):
+ return _encode_variant3(obj)
+ else:
+ return _encode_variant1(obj)
class BcdAdapter(Adapter):
"""convert a bytes() type to a string of BCD nibbles."""
@@ -44,6 +244,23 @@ class BcdAdapter(Adapter):
def _encode(self, obj, context, path):
return h2b(swap_nibbles(obj))
+class PlmnAdapter(BcdAdapter):
+ """convert a bytes(3) type to BCD string like 262-02 or 262-002."""
+ def _decode(self, obj, context, path):
+ bcd = super()._decode(obj, context, path)
+ if bcd[3] == 'f':
+ return '-'.join([bcd[:3], bcd[4:]])
+ else:
+ return '-'.join([bcd[:3], bcd[3:]])
+
+ def _encode(self, obj, context, path):
+ l = obj.split('-')
+ if len(l[1]) == 2:
+ bcd = l[0] + 'f' + l[1]
+ else:
+ bcd = l[0] + l[1]
+ return super()._encode(bcd, context, path)
+
class InvertAdapter(Adapter):
"""inverse logic (false->true, true->false)."""
@staticmethod
@@ -52,9 +269,9 @@ class InvertAdapter(Adapter):
# skip all private entries
if k.startswith('_'):
continue
- if v == False:
+ if v is False:
obj[k] = True
- elif v == True:
+ elif v is True:
obj[k] = False
return obj
@@ -125,6 +342,63 @@ class GsmStringAdapter(Adapter):
def _encode(self, obj, context, path):
return obj.encode(self.codec, self.err)
+class Ipv4Adapter(Adapter):
+ """
+ Encoder converts from 4 bytes to string representation (A.B.C.D).
+ Decoder converts from string representation (A.B.C.D) to four bytes.
+ """
+ def _decode(self, obj, context, path):
+ ia = ipaddress.IPv4Address(obj)
+ return ia.compressed
+
+ def _encode(self, obj, context, path):
+ ia = ipaddress.IPv4Address(obj)
+ return ia.packed
+
+class Ipv6Adapter(Adapter):
+ """
+ Encoder converts from 16 bytes to string representation.
+ Decoder converts from string representation to 16 bytes.
+ """
+ def _decode(self, obj, context, path):
+ ia = ipaddress.IPv6Address(obj)
+ return ia.compressed
+
+ def _encode(self, obj, context, path):
+ ia = ipaddress.IPv6Address(obj)
+ return ia.packed
+
+class StripTrailerAdapter(Adapter):
+ """
+ Encoder removes all trailing bytes matching the default_value
+ Decoder pads input data up to total_length with default_value
+
+ This is used in constellations like "FlagsEnum(StripTrailerAdapter(GreedyBytes, 3), ..."
+ where you have a bit-mask that may have 1, 2 or 3 bytes, depending on whether or not any
+ of the LSBs are actually set.
+ """
+ def __init__(self, subcon, total_length:int, default_value=b'\x00', min_len=1):
+ super().__init__(subcon)
+ assert len(default_value) == 1
+ self.total_length = total_length
+ self.default_value = default_value
+ self.min_len = min_len
+
+ def _decode(self, obj, context, path):
+ assert isinstance(obj, bytes)
+ # pad with suppressed/missing bytes
+ if len(obj) < self.total_length:
+ obj += self.default_value * (self.total_length - len(obj))
+ return int.from_bytes(obj, 'big')
+
+ def _encode(self, obj, context, path):
+ assert isinstance(obj, int)
+ obj = obj.to_bytes(self.total_length, 'big')
+ # remove trailing bytes if they are zero
+ while len(obj) > self.min_len and obj[-1] == self.default_value[0]:
+ obj = obj[:-1]
+ return obj
+
def filter_dict(d, exclude_prefix='_'):
"""filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
@@ -134,20 +408,20 @@ def filter_dict(d, exclude_prefix='_'):
for (key, value) in d.items():
if key.startswith(exclude_prefix):
continue
- if type(value) is dict:
+ if isinstance(value, dict):
res[key] = filter_dict(value)
else:
res[key] = value
return res
-def normalize_construct(c):
+def normalize_construct(c, exclude_prefix: str = '_'):
"""Convert a construct specific type to a related base type, mostly useful
so we can serialize it."""
# we need to include the filter_dict as we otherwise get elements like this
# in the dict: '_io': <_io.BytesIO object at 0x7fdb64e05860> which we cannot json-serialize
- c = filter_dict(c)
- if isinstance(c, Container) or isinstance(c, dict):
+ c = filter_dict(c, exclude_prefix)
+ if isinstance(c, (Container, dict)):
r = {k: normalize_construct(v) for (k, v) in c.items()}
elif isinstance(c, ListContainer):
r = [normalize_construct(x) for x in c]
@@ -160,13 +434,25 @@ def normalize_construct(c):
return r
-def parse_construct(c, raw_bin_data: bytes, length: typing.Optional[int] = None, exclude_prefix: str = '_'):
+def parse_construct(c, raw_bin_data: bytes, length: typing.Optional[int] = None, exclude_prefix: str = '_', context: dict = {}):
"""Helper function to wrap around normalize_construct() and filter_dict()."""
if not length:
length = len(raw_bin_data)
- parsed = c.parse(raw_bin_data, total_len=length)
- return normalize_construct(parsed)
+ try:
+ parsed = c.parse(raw_bin_data, total_len=length, **context)
+ except StreamError as e:
+ # if the input is all-ff, this means the content is undefined. Let's avoid passing StreamError
+ # exceptions in those situations (which might occur if a length field 0xff is 255 but then there's
+ # actually less bytes in the remainder of the file.
+ if all(v == 0xff for v in raw_bin_data):
+ return None
+ else:
+ raise e
+ return normalize_construct(parsed, exclude_prefix)
+def build_construct(c, decoded_data, context: dict = {}):
+ """Helper function to handle total_len."""
+ return c.build(decoded_data, total_len=None, **context)
# here we collect some shared / common definitions of data types
LV = Prefixed(Int8ub, HexAdapter(GreedyBytes))
@@ -229,6 +515,20 @@ def GsmString(n):
'''
return GsmStringAdapter(Rpad(Bytes(n), pattern=b'\xff'), codec='gsm03.38')
+def GsmOrUcs2String(n):
+ '''
+ GSM 03.38 or UCS-2 (TS 102 221 Annex A) encoded byte string of fixed length n.
+ Encoder appends padding bytes (b'\\xff') to maintain
+ length. Decoder removes those trailing bytes.
+
+ Exceptions are raised for invalid characters
+ and length excess.
+
+ Parameters:
+ n (Integer): Fixed length of the encoded byte string
+ '''
+ return GsmOrUcs2Adapter(Rpad(Bytes(n), pattern=b'\xff'))
+
class GreedyInteger(Construct):
"""A variable-length integer implementation, think of combining GrredyBytes with BytesInteger."""
def __init__(self, signed=False, swapped=False, minlen=0):
@@ -261,8 +561,7 @@ class GreedyInteger(Construct):
# round up to the minimum number
# of bytes we anticipate
- if nbytes < minlen:
- nbytes = minlen
+ nbytes = max(nbytes, minlen)
return nbytes
@@ -273,7 +572,7 @@ class GreedyInteger(Construct):
try:
data = obj.to_bytes(length, byteorder='big', signed=self.signed)
except ValueError as e:
- raise IntegerError(str(e), path=path)
+ raise IntegerError(str(e), path=path) from e
if evaluate(self.swapped, context):
data = swapbytes(data)
stream_write(stream, data, length, path)
diff --git a/pySim/esim/__init__.py b/pySim/esim/__init__.py
new file mode 100644
index 0000000..1f7ea16
--- /dev/null
+++ b/pySim/esim/__init__.py
@@ -0,0 +1,93 @@
+import sys
+from typing import Optional
+from importlib import resources
+
+
+def compile_asn1_subdir(subdir_name:str):
+ """Helper function that compiles ASN.1 syntax from all files within given subdir"""
+ import asn1tools
+ asn_txt = ''
+ __ver = sys.version_info
+ if (__ver.major, __ver.minor) >= (3, 9):
+ for i in resources.files('pySim.esim').joinpath('asn1').joinpath(subdir_name).iterdir():
+ asn_txt += i.read_text()
+ asn_txt += "\n"
+ #else:
+ #print(resources.read_text(__name__, 'asn1/rsp.asn'))
+ return asn1tools.compile_string(asn_txt, codec='der')
+
+
+# SGP.22 section 4.1 Activation Code
+class ActivationCode:
+ def __init__(self, hostname:str, token:str, oid: Optional[str] = None, cc_required: Optional[bool] = False):
+ if '$' in hostname:
+ raise ValueError('$ sign not permitted in hostname')
+ self.hostname = hostname
+ if '$' in token:
+ raise ValueError('$ sign not permitted in token')
+ self.token = token
+ # TODO: validate OID
+ self.oid = oid
+ self.cc_required = cc_required
+ # only format 1 is specified and supported here
+ self.format = 1
+
+ @staticmethod
+ def decode_str(ac: str) -> dict:
+ if ac[0] != '1':
+ raise ValueError("Unsupported AC_Format '%s'!" % ac[0])
+ ac_elements = ac.split('$')
+ d = {
+ 'oid': None,
+ 'cc_required': False,
+ }
+ d['format'] = ac_elements.pop(0)
+ d['hostname'] = ac_elements.pop(0)
+ d['token'] = ac_elements.pop(0)
+ if len(ac_elements):
+ oid = ac_elements.pop(0)
+ if oid != '':
+ d['oid'] = oid
+ if len(ac_elements):
+ ccr = ac_elements.pop(0)
+ if ccr == '1':
+ d['cc_required'] = True
+ return d
+
+ @classmethod
+ def from_string(cls, ac: str) -> 'ActivationCode':
+ """Create new instance from SGP.22 section 4.1 string representation."""
+ d = cls.decode_str(ac)
+ return cls(d['hostname'], d['token'], d['oid'], d['cc_required'])
+
+ def to_string(self, for_qrcode:bool = False) -> str:
+ """Convert from internal representation to SGP.22 section 4.1 string representation."""
+ if for_qrcode:
+ ret = 'LPA:'
+ else:
+ ret = ''
+ ret += '%d$%s$%s' % (self.format, self.hostname, self.token)
+ if self.oid:
+ ret += '$%s' % (self.oid)
+ elif self.cc_required:
+ ret += '$'
+ if self.cc_required:
+ ret += '$1'
+ return ret
+
+ def __str__(self):
+ return self.to_string()
+
+ def to_qrcode(self):
+ """Encode internal representation to QR code."""
+ import qrcode
+ qr = qrcode.QRCode()
+ qr.add_data(self.to_string(for_qrcode=True))
+ return qr.make_image()
+
+ def __repr__(self):
+ return "ActivationCode(format=%u, hostname='%s', token='%s', oid=%s, cc_required=%s)" % (self.format,
+ self.hostname,
+ self.token,
+ self.oid,
+ self.cc_required)
diff --git a/pySim/esim/asn1/rsp/PKIX1Explicit88.asn b/pySim/esim/asn1/rsp/PKIX1Explicit88.asn
new file mode 100644
index 0000000..9284c65
--- /dev/null
+++ b/pySim/esim/asn1/rsp/PKIX1Explicit88.asn
@@ -0,0 +1,657 @@
+PKIX1Explicit88 { iso(1) identified-organization(3) dod(6) internet(1)
+ security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-explicit(18) }
+
+DEFINITIONS EXPLICIT TAGS ::=
+
+BEGIN
+
+-- EXPORTS ALL --
+
+-- IMPORTS NONE --
+
+-- UNIVERSAL Types defined in 1993 and 1998 ASN.1
+-- and required by this specification
+
+-- pycrate: UniversalString, BMPString and UTF8String already in the builtin types
+
+--UniversalString ::= [UNIVERSAL 28] IMPLICIT OCTET STRING
+ -- UniversalString is defined in ASN.1:1993
+
+--BMPString ::= [UNIVERSAL 30] IMPLICIT OCTET STRING
+ -- BMPString is the subtype of UniversalString and models
+ -- the Basic Multilingual Plane of ISO/IEC 10646
+
+--UTF8String ::= [UNIVERSAL 12] IMPLICIT OCTET STRING
+ -- The content of this type conforms to RFC 3629.
+
+-- PKIX specific OIDs
+
+id-pkix OBJECT IDENTIFIER ::=
+ { iso(1) identified-organization(3) dod(6) internet(1)
+ security(5) mechanisms(5) pkix(7) }
+
+-- PKIX arcs
+
+id-pe OBJECT IDENTIFIER ::= { id-pkix 1 }
+ -- arc for private certificate extensions
+id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
+ -- arc for policy qualifier types
+id-kp OBJECT IDENTIFIER ::= { id-pkix 3 }
+ -- arc for extended key purpose OIDS
+id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
+ -- arc for access descriptors
+
+-- policyQualifierIds for Internet policy qualifiers
+
+id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
+ -- OID for CPS qualifier
+id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
+ -- OID for user notice qualifier
+
+-- access descriptor definitions
+
+id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 }
+id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
+id-ad-timeStamping OBJECT IDENTIFIER ::= { id-ad 3 }
+id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 }
+
+-- attribute data types
+
+Attribute ::= SEQUENCE {
+ type AttributeType,
+ values SET OF AttributeValue }
+ -- at least one value is required
+
+AttributeType ::= OBJECT IDENTIFIER
+
+AttributeValue ::= ANY -- DEFINED BY AttributeType
+
+AttributeTypeAndValue ::= SEQUENCE {
+ type AttributeType,
+ value AttributeValue }
+
+-- suggested naming attributes: Definition of the following
+-- information object set may be augmented to meet local
+-- requirements. Note that deleting members of the set may
+-- prevent interoperability with conforming implementations.
+-- presented in pairs: the AttributeType followed by the
+-- type definition for the corresponding AttributeValue
+
+-- Arc for standard naming attributes
+
+id-at OBJECT IDENTIFIER ::= { joint-iso-ccitt(2) ds(5) 4 }
+
+-- Naming attributes of type X520name
+
+id-at-name AttributeType ::= { id-at 41 }
+id-at-surname AttributeType ::= { id-at 4 }
+id-at-givenName AttributeType ::= { id-at 42 }
+id-at-initials AttributeType ::= { id-at 43 }
+id-at-generationQualifier AttributeType ::= { id-at 44 }
+
+-- Naming attributes of type X520Name:
+-- X520name ::= DirectoryString (SIZE (1..ub-name))
+--
+-- Expanded to avoid parameterized type:
+X520name ::= CHOICE {
+ teletexString TeletexString (SIZE (1..ub-name)),
+ printableString PrintableString (SIZE (1..ub-name)),
+ universalString UniversalString (SIZE (1..ub-name)),
+ utf8String UTF8String (SIZE (1..ub-name)),
+ bmpString BMPString (SIZE (1..ub-name)) }
+
+-- Naming attributes of type X520CommonName
+
+id-at-commonName AttributeType ::= { id-at 3 }
+
+-- Naming attributes of type X520CommonName:
+-- X520CommonName ::= DirectoryName (SIZE (1..ub-common-name))
+--
+-- Expanded to avoid parameterized type:
+X520CommonName ::= CHOICE {
+ teletexString TeletexString (SIZE (1..ub-common-name)),
+ printableString PrintableString (SIZE (1..ub-common-name)),
+ universalString UniversalString (SIZE (1..ub-common-name)),
+ utf8String UTF8String (SIZE (1..ub-common-name)),
+ bmpString BMPString (SIZE (1..ub-common-name)) }
+
+-- Naming attributes of type X520LocalityName
+
+id-at-localityName AttributeType ::= { id-at 7 }
+
+-- Naming attributes of type X520LocalityName:
+-- X520LocalityName ::= DirectoryName (SIZE (1..ub-locality-name))
+--
+-- Expanded to avoid parameterized type:
+X520LocalityName ::= CHOICE {
+ teletexString TeletexString (SIZE (1..ub-locality-name)),
+ printableString PrintableString (SIZE (1..ub-locality-name)),
+ universalString UniversalString (SIZE (1..ub-locality-name)),
+ utf8String UTF8String (SIZE (1..ub-locality-name)),
+ bmpString BMPString (SIZE (1..ub-locality-name)) }
+
+-- Naming attributes of type X520StateOrProvinceName
+
+id-at-stateOrProvinceName AttributeType ::= { id-at 8 }
+
+-- Naming attributes of type X520StateOrProvinceName:
+-- X520StateOrProvinceName ::= DirectoryName (SIZE (1..ub-state-name))
+--
+-- Expanded to avoid parameterized type:
+X520StateOrProvinceName ::= CHOICE {
+ teletexString TeletexString (SIZE (1..ub-state-name)),
+ printableString PrintableString (SIZE (1..ub-state-name)),
+ universalString UniversalString (SIZE (1..ub-state-name)),
+ utf8String UTF8String (SIZE (1..ub-state-name)),
+ bmpString BMPString (SIZE (1..ub-state-name)) }
+
+-- Naming attributes of type X520OrganizationName
+
+id-at-organizationName AttributeType ::= { id-at 10 }
+
+-- Naming attributes of type X520OrganizationName:
+-- X520OrganizationName ::=
+-- DirectoryName (SIZE (1..ub-organization-name))
+--
+-- Expanded to avoid parameterized type:
+X520OrganizationName ::= CHOICE {
+ teletexString TeletexString
+ (SIZE (1..ub-organization-name)),
+ printableString PrintableString
+ (SIZE (1..ub-organization-name)),
+ universalString UniversalString
+ (SIZE (1..ub-organization-name)),
+ utf8String UTF8String
+ (SIZE (1..ub-organization-name)),
+ bmpString BMPString
+ (SIZE (1..ub-organization-name)) }
+
+-- Naming attributes of type X520OrganizationalUnitName
+
+id-at-organizationalUnitName AttributeType ::= { id-at 11 }
+
+-- Naming attributes of type X520OrganizationalUnitName:
+-- X520OrganizationalUnitName ::=
+-- DirectoryName (SIZE (1..ub-organizational-unit-name))
+--
+-- Expanded to avoid parameterized type:
+X520OrganizationalUnitName ::= CHOICE {
+ teletexString TeletexString
+ (SIZE (1..ub-organizational-unit-name)),
+ printableString PrintableString
+ (SIZE (1..ub-organizational-unit-name)),
+ universalString UniversalString
+ (SIZE (1..ub-organizational-unit-name)),
+ utf8String UTF8String
+ (SIZE (1..ub-organizational-unit-name)),
+ bmpString BMPString
+ (SIZE (1..ub-organizational-unit-name)) }
+
+-- Naming attributes of type X520Title
+
+id-at-title AttributeType ::= { id-at 12 }
+
+-- Naming attributes of type X520Title:
+-- X520Title ::= DirectoryName (SIZE (1..ub-title))
+--
+-- Expanded to avoid parameterized type:
+X520Title ::= CHOICE {
+ teletexString TeletexString (SIZE (1..ub-title)),
+ printableString PrintableString (SIZE (1..ub-title)),
+ universalString UniversalString (SIZE (1..ub-title)),
+ utf8String UTF8String (SIZE (1..ub-title)),
+ bmpString BMPString (SIZE (1..ub-title)) }
+
+-- Naming attributes of type X520dnQualifier
+
+id-at-dnQualifier AttributeType ::= { id-at 46 }
+
+X520dnQualifier ::= PrintableString
+
+-- Naming attributes of type X520countryName (digraph from IS 3166)
+
+id-at-countryName AttributeType ::= { id-at 6 }
+
+X520countryName ::= PrintableString (SIZE (2))
+
+-- Naming attributes of type X520SerialNumber
+
+id-at-serialNumber AttributeType ::= { id-at 5 }
+
+X520SerialNumber ::= PrintableString (SIZE (1..ub-serial-number))
+
+-- Naming attributes of type X520Pseudonym
+
+id-at-pseudonym AttributeType ::= { id-at 65 }
+
+-- Naming attributes of type X520Pseudonym:
+-- X520Pseudonym ::= DirectoryName (SIZE (1..ub-pseudonym))
+--
+-- Expanded to avoid parameterized type:
+X520Pseudonym ::= CHOICE {
+ teletexString TeletexString (SIZE (1..ub-pseudonym)),
+ printableString PrintableString (SIZE (1..ub-pseudonym)),
+ universalString UniversalString (SIZE (1..ub-pseudonym)),
+ utf8String UTF8String (SIZE (1..ub-pseudonym)),
+ bmpString BMPString (SIZE (1..ub-pseudonym)) }
+
+-- Naming attributes of type DomainComponent (from RFC 4519)
+
+id-domainComponent AttributeType ::= { 0 9 2342 19200300 100 1 25 }
+
+DomainComponent ::= IA5String
+
+-- Legacy attributes
+
+pkcs-9 OBJECT IDENTIFIER ::=
+ { iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 9 }
+
+id-emailAddress AttributeType ::= { pkcs-9 1 }
+
+EmailAddress ::= IA5String (SIZE (1..ub-emailaddress-length))
+
+-- naming data types --
+
+Name ::= CHOICE { -- only one possibility for now --
+ rdnSequence RDNSequence }
+
+RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
+
+DistinguishedName ::= RDNSequence
+
+RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
+
+-- Directory string type --
+
+DirectoryString ::= CHOICE {
+ teletexString TeletexString (SIZE (1..MAX)),
+ printableString PrintableString (SIZE (1..MAX)),
+ universalString UniversalString (SIZE (1..MAX)),
+ utf8String UTF8String (SIZE (1..MAX)),
+ bmpString BMPString (SIZE (1..MAX)) }
+
+-- certificate and CRL specific structures begin here
+
+Certificate ::= SEQUENCE {
+ tbsCertificate TBSCertificate,
+ signatureAlgorithm AlgorithmIdentifier,
+ signature BIT STRING }
+
+TBSCertificate ::= SEQUENCE {
+ version [0] Version DEFAULT v1,
+ serialNumber CertificateSerialNumber,
+ signature AlgorithmIdentifier,
+ issuer Name,
+ validity Validity,
+ subject Name,
+ subjectPublicKeyInfo SubjectPublicKeyInfo,
+ issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
+ -- If present, version MUST be v2 or v3
+ subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
+ -- If present, version MUST be v2 or v3
+ extensions [3] Extensions OPTIONAL
+ -- If present, version MUST be v3 -- }
+
+Version ::= INTEGER { v1(0), v2(1), v3(2) }
+
+CertificateSerialNumber ::= INTEGER
+
+Validity ::= SEQUENCE {
+ notBefore Time,
+ notAfter Time }
+
+Time ::= CHOICE {
+ utcTime UTCTime,
+ generalTime GeneralizedTime }
+
+UniqueIdentifier ::= BIT STRING
+
+SubjectPublicKeyInfo ::= SEQUENCE {
+ algorithm AlgorithmIdentifier,
+ subjectPublicKey BIT STRING }
+
+Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
+
+Extension ::= SEQUENCE {
+ extnID OBJECT IDENTIFIER,
+ critical BOOLEAN DEFAULT FALSE,
+ extnValue OCTET STRING
+ -- contains the DER encoding of an ASN.1 value
+ -- corresponding to the extension type identified
+ -- by extnID
+ }
+
+-- CRL structures
+
+CertificateList ::= SEQUENCE {
+ tbsCertList TBSCertList,
+ signatureAlgorithm AlgorithmIdentifier,
+ signature BIT STRING }
+
+TBSCertList ::= SEQUENCE {
+ version Version OPTIONAL,
+ -- if present, MUST be v2
+ signature AlgorithmIdentifier,
+ issuer Name,
+ thisUpdate Time,
+ nextUpdate Time OPTIONAL,
+ revokedCertificates SEQUENCE OF SEQUENCE {
+ userCertificate CertificateSerialNumber,
+ revocationDate Time,
+ crlEntryExtensions Extensions OPTIONAL
+ -- if present, version MUST be v2
+ } OPTIONAL,
+ crlExtensions [0] Extensions OPTIONAL }
+ -- if present, version MUST be v2
+
+-- Version, Time, CertificateSerialNumber, and Extensions were
+-- defined earlier for use in the certificate structure
+
+AlgorithmIdentifier ::= SEQUENCE {
+ algorithm OBJECT IDENTIFIER,
+ parameters ANY DEFINED BY algorithm OPTIONAL }
+ -- contains a value of the type
+ -- registered for use with the
+ -- algorithm object identifier value
+
+-- X.400 address syntax starts here
+
+ORAddress ::= SEQUENCE {
+ built-in-standard-attributes BuiltInStandardAttributes,
+ built-in-domain-defined-attributes
+ BuiltInDomainDefinedAttributes OPTIONAL,
+ -- see also teletex-domain-defined-attributes
+ extension-attributes ExtensionAttributes OPTIONAL }
+
+-- Built-in Standard Attributes
+
+BuiltInStandardAttributes ::= SEQUENCE {
+ country-name CountryName OPTIONAL,
+ administration-domain-name AdministrationDomainName OPTIONAL,
+ network-address [0] IMPLICIT NetworkAddress OPTIONAL,
+ -- see also extended-network-address
+ terminal-identifier [1] IMPLICIT TerminalIdentifier OPTIONAL,
+ private-domain-name [2] PrivateDomainName OPTIONAL,
+ organization-name [3] IMPLICIT OrganizationName OPTIONAL,
+ -- see also teletex-organization-name
+ numeric-user-identifier [4] IMPLICIT NumericUserIdentifier
+ OPTIONAL,
+ personal-name [5] IMPLICIT PersonalName OPTIONAL,
+ -- see also teletex-personal-name
+ organizational-unit-names [6] IMPLICIT OrganizationalUnitNames
+ OPTIONAL }
+ -- see also teletex-organizational-unit-names
+
+CountryName ::= [APPLICATION 1] CHOICE {
+ x121-dcc-code NumericString
+ (SIZE (ub-country-name-numeric-length)),
+ iso-3166-alpha2-code PrintableString
+ (SIZE (ub-country-name-alpha-length)) }
+
+AdministrationDomainName ::= [APPLICATION 2] CHOICE {
+ numeric NumericString (SIZE (0..ub-domain-name-length)),
+ printable PrintableString (SIZE (0..ub-domain-name-length)) }
+
+NetworkAddress ::= X121Address -- see also extended-network-address
+
+X121Address ::= NumericString (SIZE (1..ub-x121-address-length))
+
+TerminalIdentifier ::= PrintableString (SIZE (1..ub-terminal-id-length))
+
+PrivateDomainName ::= CHOICE {
+ numeric NumericString (SIZE (1..ub-domain-name-length)),
+ printable PrintableString (SIZE (1..ub-domain-name-length)) }
+
+OrganizationName ::= PrintableString
+ (SIZE (1..ub-organization-name-length))
+ -- see also teletex-organization-name
+
+NumericUserIdentifier ::= NumericString
+ (SIZE (1..ub-numeric-user-id-length))
+
+PersonalName ::= SET {
+ surname [0] IMPLICIT PrintableString
+ (SIZE (1..ub-surname-length)),
+ given-name [1] IMPLICIT PrintableString
+ (SIZE (1..ub-given-name-length)) OPTIONAL,
+ initials [2] IMPLICIT PrintableString
+ (SIZE (1..ub-initials-length)) OPTIONAL,
+ generation-qualifier [3] IMPLICIT PrintableString
+ (SIZE (1..ub-generation-qualifier-length))
+ OPTIONAL }
+ -- see also teletex-personal-name
+
+OrganizationalUnitNames ::= SEQUENCE SIZE (1..ub-organizational-units)
+ OF OrganizationalUnitName
+ -- see also teletex-organizational-unit-names
+
+OrganizationalUnitName ::= PrintableString (SIZE
+ (1..ub-organizational-unit-name-length))
+
+-- Built-in Domain-defined Attributes
+
+BuiltInDomainDefinedAttributes ::= SEQUENCE SIZE
+ (1..ub-domain-defined-attributes) OF
+ BuiltInDomainDefinedAttribute
+
+BuiltInDomainDefinedAttribute ::= SEQUENCE {
+ type PrintableString (SIZE
+ (1..ub-domain-defined-attribute-type-length)),
+ value PrintableString (SIZE
+ (1..ub-domain-defined-attribute-value-length)) }
+
+-- Extension Attributes
+
+ExtensionAttributes ::= SET SIZE (1..ub-extension-attributes) OF
+ ExtensionAttribute
+
+ExtensionAttribute ::= SEQUENCE {
+ extension-attribute-type [0] IMPLICIT INTEGER
+ (0..ub-extension-attributes),
+ extension-attribute-value [1]
+ ANY DEFINED BY extension-attribute-type }
+
+-- Extension types and attribute values
+
+common-name INTEGER ::= 1
+
+CommonName ::= PrintableString (SIZE (1..ub-common-name-length))
+
+teletex-common-name INTEGER ::= 2
+
+TeletexCommonName ::= TeletexString (SIZE (1..ub-common-name-length))
+
+teletex-organization-name INTEGER ::= 3
+
+TeletexOrganizationName ::=
+ TeletexString (SIZE (1..ub-organization-name-length))
+
+teletex-personal-name INTEGER ::= 4
+
+TeletexPersonalName ::= SET {
+ surname [0] IMPLICIT TeletexString
+ (SIZE (1..ub-surname-length)),
+ given-name [1] IMPLICIT TeletexString
+ (SIZE (1..ub-given-name-length)) OPTIONAL,
+ initials [2] IMPLICIT TeletexString
+ (SIZE (1..ub-initials-length)) OPTIONAL,
+ generation-qualifier [3] IMPLICIT TeletexString
+ (SIZE (1..ub-generation-qualifier-length))
+ OPTIONAL }
+
+teletex-organizational-unit-names INTEGER ::= 5
+
+TeletexOrganizationalUnitNames ::= SEQUENCE SIZE
+ (1..ub-organizational-units) OF TeletexOrganizationalUnitName
+
+TeletexOrganizationalUnitName ::= TeletexString
+ (SIZE (1..ub-organizational-unit-name-length))
+
+pds-name INTEGER ::= 7
+
+PDSName ::= PrintableString (SIZE (1..ub-pds-name-length))
+
+physical-delivery-country-name INTEGER ::= 8
+
+PhysicalDeliveryCountryName ::= CHOICE {
+ x121-dcc-code NumericString (SIZE (ub-country-name-numeric-length)),
+ iso-3166-alpha2-code PrintableString
+ (SIZE (ub-country-name-alpha-length)) }
+
+postal-code INTEGER ::= 9
+
+PostalCode ::= CHOICE {
+ numeric-code NumericString (SIZE (1..ub-postal-code-length)),
+ printable-code PrintableString (SIZE (1..ub-postal-code-length)) }
+
+physical-delivery-office-name INTEGER ::= 10
+
+PhysicalDeliveryOfficeName ::= PDSParameter
+
+physical-delivery-office-number INTEGER ::= 11
+
+PhysicalDeliveryOfficeNumber ::= PDSParameter
+
+extension-OR-address-components INTEGER ::= 12
+
+ExtensionORAddressComponents ::= PDSParameter
+
+physical-delivery-personal-name INTEGER ::= 13
+
+PhysicalDeliveryPersonalName ::= PDSParameter
+
+physical-delivery-organization-name INTEGER ::= 14
+
+PhysicalDeliveryOrganizationName ::= PDSParameter
+
+extension-physical-delivery-address-components INTEGER ::= 15
+
+ExtensionPhysicalDeliveryAddressComponents ::= PDSParameter
+
+unformatted-postal-address INTEGER ::= 16
+
+UnformattedPostalAddress ::= SET {
+ printable-address SEQUENCE SIZE (1..ub-pds-physical-address-lines)
+ OF PrintableString (SIZE (1..ub-pds-parameter-length)) OPTIONAL,
+ teletex-string TeletexString
+ (SIZE (1..ub-unformatted-address-length)) OPTIONAL }
+
+street-address INTEGER ::= 17
+
+StreetAddress ::= PDSParameter
+
+post-office-box-address INTEGER ::= 18
+
+PostOfficeBoxAddress ::= PDSParameter
+
+poste-restante-address INTEGER ::= 19
+
+PosteRestanteAddress ::= PDSParameter
+
+unique-postal-name INTEGER ::= 20
+
+UniquePostalName ::= PDSParameter
+
+local-postal-attributes INTEGER ::= 21
+
+LocalPostalAttributes ::= PDSParameter
+
+PDSParameter ::= SET {
+ printable-string PrintableString
+ (SIZE(1..ub-pds-parameter-length)) OPTIONAL,
+ teletex-string TeletexString
+ (SIZE(1..ub-pds-parameter-length)) OPTIONAL }
+
+extended-network-address INTEGER ::= 22
+
+ExtendedNetworkAddress ::= CHOICE {
+ e163-4-address SEQUENCE {
+ number [0] IMPLICIT NumericString
+ (SIZE (1..ub-e163-4-number-length)),
+ sub-address [1] IMPLICIT NumericString
+ (SIZE (1..ub-e163-4-sub-address-length))
+ OPTIONAL },
+ psap-address [0] IMPLICIT PresentationAddress }
+
+PresentationAddress ::= SEQUENCE {
+ pSelector [0] EXPLICIT OCTET STRING OPTIONAL,
+ sSelector [1] EXPLICIT OCTET STRING OPTIONAL,
+ tSelector [2] EXPLICIT OCTET STRING OPTIONAL,
+ nAddresses [3] EXPLICIT SET SIZE (1..MAX) OF OCTET STRING }
+
+terminal-type INTEGER ::= 23
+
+TerminalType ::= INTEGER {
+ telex (3),
+ teletex (4),
+ g3-facsimile (5),
+ g4-facsimile (6),
+ ia5-terminal (7),
+ videotex (8) } (0..ub-integer-options)
+
+-- Extension Domain-defined Attributes
+
+teletex-domain-defined-attributes INTEGER ::= 6
+
+TeletexDomainDefinedAttributes ::= SEQUENCE SIZE
+ (1..ub-domain-defined-attributes) OF TeletexDomainDefinedAttribute
+
+TeletexDomainDefinedAttribute ::= SEQUENCE {
+ type TeletexString
+ (SIZE (1..ub-domain-defined-attribute-type-length)),
+ value TeletexString
+ (SIZE (1..ub-domain-defined-attribute-value-length)) }
+
+-- specifications of Upper Bounds MUST be regarded as mandatory
+-- from Annex B of ITU-T X.411 Reference Definition of MTS Parameter
+-- Upper Bounds
+
+-- Upper Bounds
+ub-name INTEGER ::= 32768
+ub-common-name INTEGER ::= 64
+ub-locality-name INTEGER ::= 128
+ub-state-name INTEGER ::= 128
+ub-organization-name INTEGER ::= 64
+ub-organizational-unit-name INTEGER ::= 64
+ub-title INTEGER ::= 64
+ub-serial-number INTEGER ::= 64
+ub-match INTEGER ::= 128
+ub-emailaddress-length INTEGER ::= 255
+ub-common-name-length INTEGER ::= 64
+ub-country-name-alpha-length INTEGER ::= 2
+ub-country-name-numeric-length INTEGER ::= 3
+ub-domain-defined-attributes INTEGER ::= 4
+ub-domain-defined-attribute-type-length INTEGER ::= 8
+ub-domain-defined-attribute-value-length INTEGER ::= 128
+ub-domain-name-length INTEGER ::= 16
+ub-extension-attributes INTEGER ::= 256
+ub-e163-4-number-length INTEGER ::= 15
+ub-e163-4-sub-address-length INTEGER ::= 40
+ub-generation-qualifier-length INTEGER ::= 3
+ub-given-name-length INTEGER ::= 16
+ub-initials-length INTEGER ::= 5
+ub-integer-options INTEGER ::= 256
+ub-numeric-user-id-length INTEGER ::= 32
+ub-organization-name-length INTEGER ::= 64
+ub-organizational-unit-name-length INTEGER ::= 32
+ub-organizational-units INTEGER ::= 4
+ub-pds-name-length INTEGER ::= 16
+ub-pds-parameter-length INTEGER ::= 30
+ub-pds-physical-address-lines INTEGER ::= 6
+ub-postal-code-length INTEGER ::= 16
+ub-pseudonym INTEGER ::= 128
+ub-surname-length INTEGER ::= 40
+ub-terminal-id-length INTEGER ::= 24
+ub-unformatted-address-length INTEGER ::= 180
+ub-x121-address-length INTEGER ::= 16
+
+-- Note - upper bounds on string types, such as TeletexString, are
+-- measured in characters. Excepting PrintableString or IA5String, a
+-- significantly greater number of octets will be required to hold
+-- such a value. As a minimum, 16 octets, or twice the specified
+-- upper bound, whichever is the larger, should be allowed for
+-- TeletexString. For UTF8String or UniversalString at least four
+-- times the upper bound should be allowed.
+
+END
+
diff --git a/pySim/esim/asn1/rsp/PKIX1Implicit88.asn b/pySim/esim/asn1/rsp/PKIX1Implicit88.asn
new file mode 100644
index 0000000..aafd785
--- /dev/null
+++ b/pySim/esim/asn1/rsp/PKIX1Implicit88.asn
@@ -0,0 +1,343 @@
+PKIX1Implicit88 { iso(1) identified-organization(3) dod(6) internet(1)
+ security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-implicit(19) }
+
+DEFINITIONS IMPLICIT TAGS ::=
+
+BEGIN
+
+-- EXPORTS ALL --
+
+IMPORTS
+ id-pe, id-kp, id-qt-unotice, id-qt-cps,
+ ORAddress, Name, RelativeDistinguishedName,
+ CertificateSerialNumber, Attribute, DirectoryString
+ FROM PKIX1Explicit88 { iso(1) identified-organization(3)
+ dod(6) internet(1) security(5) mechanisms(5) pkix(7)
+ id-mod(0) id-pkix1-explicit(18) };
+
+-- ISO arc for standard certificate and CRL extensions
+
+id-ce OBJECT IDENTIFIER ::= {joint-iso-ccitt(2) ds(5) 29}
+
+-- authority key identifier OID and syntax
+
+id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
+
+AuthorityKeyIdentifier ::= SEQUENCE {
+ keyIdentifier [0] KeyIdentifier OPTIONAL,
+ authorityCertIssuer [1] GeneralNames OPTIONAL,
+ authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL }
+ -- authorityCertIssuer and authorityCertSerialNumber MUST both
+ -- be present or both be absent
+
+KeyIdentifier ::= OCTET STRING
+
+-- subject key identifier OID and syntax
+
+id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
+
+SubjectKeyIdentifier ::= KeyIdentifier
+
+-- key usage extension OID and syntax
+
+id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
+
+KeyUsage ::= BIT STRING {
+ digitalSignature (0),
+ nonRepudiation (1), -- recent editions of X.509 have
+ -- renamed this bit to contentCommitment
+ keyEncipherment (2),
+ dataEncipherment (3),
+ keyAgreement (4),
+ keyCertSign (5),
+ cRLSign (6),
+ encipherOnly (7),
+ decipherOnly (8) }
+
+-- private key usage period extension OID and syntax
+
+id-ce-privateKeyUsagePeriod OBJECT IDENTIFIER ::= { id-ce 16 }
+
+PrivateKeyUsagePeriod ::= SEQUENCE {
+ notBefore [0] GeneralizedTime OPTIONAL,
+ notAfter [1] GeneralizedTime OPTIONAL }
+ -- either notBefore or notAfter MUST be present
+
+-- certificate policies extension OID and syntax
+
+id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
+
+anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
+
+CertificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
+
+PolicyInformation ::= SEQUENCE {
+ policyIdentifier CertPolicyId,
+ policyQualifiers SEQUENCE SIZE (1..MAX) OF
+ PolicyQualifierInfo OPTIONAL }
+
+CertPolicyId ::= OBJECT IDENTIFIER
+
+PolicyQualifierInfo ::= SEQUENCE {
+ policyQualifierId PolicyQualifierId,
+ qualifier ANY DEFINED BY policyQualifierId }
+
+-- Implementations that recognize additional policy qualifiers MUST
+-- augment the following definition for PolicyQualifierId
+
+PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
+
+-- CPS pointer qualifier
+
+CPSuri ::= IA5String
+
+-- user notice qualifier
+
+UserNotice ::= SEQUENCE {
+ noticeRef NoticeReference OPTIONAL,
+ explicitText DisplayText OPTIONAL }
+
+NoticeReference ::= SEQUENCE {
+ organization DisplayText,
+ noticeNumbers SEQUENCE OF INTEGER }
+
+DisplayText ::= CHOICE {
+ ia5String IA5String (SIZE (1..200)),
+ visibleString VisibleString (SIZE (1..200)),
+ bmpString BMPString (SIZE (1..200)),
+ utf8String UTF8String (SIZE (1..200)) }
+
+-- policy mapping extension OID and syntax
+
+id-ce-policyMappings OBJECT IDENTIFIER ::= { id-ce 33 }
+
+PolicyMappings ::= SEQUENCE SIZE (1..MAX) OF SEQUENCE {
+ issuerDomainPolicy CertPolicyId,
+ subjectDomainPolicy CertPolicyId }
+
+-- subject alternative name extension OID and syntax
+
+id-ce-subjectAltName OBJECT IDENTIFIER ::= { id-ce 17 }
+
+SubjectAltName ::= GeneralNames
+
+GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
+
+GeneralName ::= CHOICE {
+ otherName [0] AnotherName,
+ rfc822Name [1] IA5String,
+ dNSName [2] IA5String,
+ x400Address [3] ORAddress,
+ directoryName [4] Name,
+ ediPartyName [5] EDIPartyName,
+ uniformResourceIdentifier [6] IA5String,
+ iPAddress [7] OCTET STRING,
+ registeredID [8] OBJECT IDENTIFIER }
+
+-- AnotherName replaces OTHER-NAME ::= TYPE-IDENTIFIER, as
+-- TYPE-IDENTIFIER is not supported in the '88 ASN.1 syntax
+
+AnotherName ::= SEQUENCE {
+ type-id OBJECT IDENTIFIER,
+ value [0] EXPLICIT ANY DEFINED BY type-id }
+
+EDIPartyName ::= SEQUENCE {
+ nameAssigner [0] DirectoryString OPTIONAL,
+ partyName [1] DirectoryString }
+
+-- issuer alternative name extension OID and syntax
+
+id-ce-issuerAltName OBJECT IDENTIFIER ::= { id-ce 18 }
+
+IssuerAltName ::= GeneralNames
+
+id-ce-subjectDirectoryAttributes OBJECT IDENTIFIER ::= { id-ce 9 }
+
+SubjectDirectoryAttributes ::= SEQUENCE SIZE (1..MAX) OF Attribute
+
+-- basic constraints extension OID and syntax
+
+id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
+
+BasicConstraints ::= SEQUENCE {
+ cA BOOLEAN DEFAULT FALSE,
+ pathLenConstraint INTEGER (0..MAX) OPTIONAL }
+
+-- name constraints extension OID and syntax
+
+id-ce-nameConstraints OBJECT IDENTIFIER ::= { id-ce 30 }
+
+NameConstraints ::= SEQUENCE {
+ permittedSubtrees [0] GeneralSubtrees OPTIONAL,
+ excludedSubtrees [1] GeneralSubtrees OPTIONAL }
+
+GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
+
+GeneralSubtree ::= SEQUENCE {
+ base GeneralName,
+ minimum [0] BaseDistance DEFAULT 0,
+ maximum [1] BaseDistance OPTIONAL }
+
+BaseDistance ::= INTEGER (0..MAX)
+
+-- policy constraints extension OID and syntax
+
+id-ce-policyConstraints OBJECT IDENTIFIER ::= { id-ce 36 }
+
+PolicyConstraints ::= SEQUENCE {
+ requireExplicitPolicy [0] SkipCerts OPTIONAL,
+ inhibitPolicyMapping [1] SkipCerts OPTIONAL }
+
+SkipCerts ::= INTEGER (0..MAX)
+
+-- CRL distribution points extension OID and syntax
+
+id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= {id-ce 31}
+
+CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
+
+DistributionPoint ::= SEQUENCE {
+ distributionPoint [0] DistributionPointName OPTIONAL,
+ reasons [1] ReasonFlags OPTIONAL,
+ cRLIssuer [2] GeneralNames OPTIONAL }
+
+DistributionPointName ::= CHOICE {
+ fullName [0] GeneralNames,
+ nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
+
+ReasonFlags ::= BIT STRING {
+ unused (0),
+ keyCompromise (1),
+ cACompromise (2),
+ affiliationChanged (3),
+ superseded (4),
+ cessationOfOperation (5),
+ certificateHold (6),
+ privilegeWithdrawn (7),
+ aACompromise (8) }
+
+-- extended key usage extension OID and syntax
+
+id-ce-extKeyUsage OBJECT IDENTIFIER ::= {id-ce 37}
+
+ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
+
+KeyPurposeId ::= OBJECT IDENTIFIER
+
+-- permit unspecified key uses
+
+anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 }
+
+-- extended key purpose OIDs
+
+id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 }
+id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 }
+id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 }
+id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 }
+id-kp-timeStamping OBJECT IDENTIFIER ::= { id-kp 8 }
+id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 }
+
+-- inhibit any policy OID and syntax
+
+id-ce-inhibitAnyPolicy OBJECT IDENTIFIER ::= { id-ce 54 }
+
+InhibitAnyPolicy ::= SkipCerts
+
+-- freshest (delta)CRL extension OID and syntax
+
+id-ce-freshestCRL OBJECT IDENTIFIER ::= { id-ce 46 }
+
+FreshestCRL ::= CRLDistributionPoints
+
+-- authority info access
+
+id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
+
+AuthorityInfoAccessSyntax ::=
+ SEQUENCE SIZE (1..MAX) OF AccessDescription
+
+AccessDescription ::= SEQUENCE {
+ accessMethod OBJECT IDENTIFIER,
+ accessLocation GeneralName }
+
+-- subject info access
+
+id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
+
+SubjectInfoAccessSyntax ::=
+ SEQUENCE SIZE (1..MAX) OF AccessDescription
+
+-- CRL number extension OID and syntax
+
+id-ce-cRLNumber OBJECT IDENTIFIER ::= { id-ce 20 }
+
+CRLNumber ::= INTEGER (0..MAX)
+
+-- issuing distribution point extension OID and syntax
+
+id-ce-issuingDistributionPoint OBJECT IDENTIFIER ::= { id-ce 28 }
+
+IssuingDistributionPoint ::= SEQUENCE {
+ distributionPoint [0] DistributionPointName OPTIONAL,
+ onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE,
+ onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE,
+ onlySomeReasons [3] ReasonFlags OPTIONAL,
+ indirectCRL [4] BOOLEAN DEFAULT FALSE,
+ onlyContainsAttributeCerts [5] BOOLEAN DEFAULT FALSE }
+ -- at most one of onlyContainsUserCerts, onlyContainsCACerts,
+ -- and onlyContainsAttributeCerts may be set to TRUE.
+
+id-ce-deltaCRLIndicator OBJECT IDENTIFIER ::= { id-ce 27 }
+
+BaseCRLNumber ::= CRLNumber
+
+-- reason code extension OID and syntax
+
+id-ce-cRLReasons OBJECT IDENTIFIER ::= { id-ce 21 }
+
+CRLReason ::= ENUMERATED {
+ unspecified (0),
+ keyCompromise (1),
+ cACompromise (2),
+ affiliationChanged (3),
+ superseded (4),
+ cessationOfOperation (5),
+ certificateHold (6),
+ removeFromCRL (8),
+ privilegeWithdrawn (9),
+ aACompromise (10) }
+
+-- certificate issuer CRL entry extension OID and syntax
+
+id-ce-certificateIssuer OBJECT IDENTIFIER ::= { id-ce 29 }
+
+CertificateIssuer ::= GeneralNames
+
+-- hold instruction extension OID and syntax
+
+id-ce-holdInstructionCode OBJECT IDENTIFIER ::= { id-ce 23 }
+
+HoldInstructionCode ::= OBJECT IDENTIFIER
+
+-- ANSI x9 arc holdinstruction arc
+
+holdInstruction OBJECT IDENTIFIER ::=
+ {joint-iso-itu-t(2) member-body(2) us(840) x9cm(10040) 2}
+
+-- ANSI X9 holdinstructions
+
+id-holdinstruction-none OBJECT IDENTIFIER ::=
+ {holdInstruction 1} -- deprecated
+
+id-holdinstruction-callissuer OBJECT IDENTIFIER ::= {holdInstruction 2}
+
+id-holdinstruction-reject OBJECT IDENTIFIER ::= {holdInstruction 3}
+
+-- invalidity date CRL entry extension OID and syntax
+
+id-ce-invalidityDate OBJECT IDENTIFIER ::= { id-ce 24 }
+
+InvalidityDate ::= GeneralizedTime
+
+END
+
diff --git a/pySim/esim/asn1/rsp/rsp.asn b/pySim/esim/asn1/rsp/rsp.asn
new file mode 100644
index 0000000..e87b74e
--- /dev/null
+++ b/pySim/esim/asn1/rsp/rsp.asn
@@ -0,0 +1,785 @@
+RSPDefinitions {joint-iso-itu-t(2) international-organizations(23) gsma(146) rsp(1) spec-version(1) version-two(2)}
+DEFINITIONS
+AUTOMATIC TAGS
+EXTENSIBILITY IMPLIED ::=
+BEGIN
+
+IMPORTS Certificate, CertificateList, Time FROM PKIX1Explicit88 {iso(1) identified-organization(3) dod(6) internet(1) security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-explicit(18)}
+SubjectKeyIdentifier FROM PKIX1Implicit88 {iso(1) identified-organization(3) dod(6) internet(1) security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-implicit(19)};
+
+id-rsp OBJECT IDENTIFIER ::= {joint-iso-itu-t(2) international-organizations(23) gsma(146) rsp(1)}
+
+-- Basic types, for size constraints
+Octet8 ::= OCTET STRING (SIZE(8))
+Octet16 ::= OCTET STRING (SIZE(16))
+OctetTo16 ::= OCTET STRING (SIZE(1..16))
+Octet32 ::= OCTET STRING (SIZE(32))
+Octet1 ::= OCTET STRING(SIZE(1))
+Octet2 ::= OCTET STRING (SIZE(2))
+VersionType ::= OCTET STRING(SIZE(3)) -- major/minor/revision version are coded as binary value on byte 1/2/3, e.g. '02 00 0C' for v2.0.12.
+Iccid ::= [APPLICATION 26] OCTET STRING (SIZE(10)) -- ICCID as coded in EFiccid, corresponding tag is '5A'
+RemoteOpId ::= [2] INTEGER {installBoundProfilePackage(1)}
+TransactionId ::= OCTET STRING (SIZE(1..16))
+
+-- Definition of EUICCInfo1 --------------------------
+GetEuiccInfo1Request ::= [32] SEQUENCE { -- Tag 'BF20'
+}
+
+EUICCInfo1 ::= [32] SEQUENCE { -- Tag 'BF20'
+ svn [2] VersionType, -- GSMA SGP.22 version supported (SVN)
+ euiccCiPKIdListForVerification [9] SEQUENCE OF SubjectKeyIdentifier, -- List of CI Public Key Identifiers supported on the eUICC for signature verification
+ euiccCiPKIdListForSigning [10] SEQUENCE OF SubjectKeyIdentifier -- List of CI Public Key Identifier supported on the eUICC for signature creation
+}
+
+-- Definition of EUICCInfo2 --------------------------
+GetEuiccInfo2Request ::= [34] SEQUENCE { -- Tag 'BF22'
+}
+
+EUICCInfo2 ::= [34] SEQUENCE { -- Tag 'BF22'
+ profileVersion [1] VersionType, -- SIMAlliance Profile package version supported
+ svn [2] VersionType, -- GSMA SGP.22 version supported (SVN)
+ euiccFirmwareVer [3] VersionType, -- eUICC Firmware version
+ extCardResource [4] OCTET STRING, -- Extended Card Resource Information according to ETSI TS 102 226
+ uiccCapability [5] UICCCapability,
+ javacardVersion [6] VersionType OPTIONAL,
+ globalplatformVersion [7] VersionType OPTIONAL,
+ rspCapability [8] RspCapability,
+ euiccCiPKIdListForVerification [9] SEQUENCE OF SubjectKeyIdentifier, -- List of CI Public Key Identifiers supported on the eUICC for signature verification
+ euiccCiPKIdListForSigning [10] SEQUENCE OF SubjectKeyIdentifier, -- List of CI Public Key Identifier supported on the eUICC for signature creation
+ euiccCategory [11] INTEGER {
+ other(0),
+ basicEuicc(1),
+ mediumEuicc(2),
+ contactlessEuicc(3)
+ } OPTIONAL,
+ forbiddenProfilePolicyRules [25] PprIds OPTIONAL, -- Tag '99'
+ ppVersion VersionType, -- Protection Profile version
+ sasAcreditationNumber UTF8String (SIZE(0..64)),
+ certificationDataObject [12] CertificationDataObject OPTIONAL
+}
+
+-- Definition of RspCapability
+RspCapability ::= BIT STRING {
+ additionalProfile(0), -- at least one more Profile can be installed
+ crlSupport(1), -- CRL
+ rpmSupport(2), -- Remote Profile Management
+ testProfileSupport (3) -- support for test profile
+}
+
+-- Definition of CertificationDataObject
+CertificationDataObject ::= SEQUENCE {
+ platformLabel UTF8String, -- Platform_Label as defined in GlobalPlatform DLOA specification [57]
+ discoveryBaseURL UTF8String -- Discovery Base URL of the SE default DLOA Registrar as defined in GlobalPlatform DLOA specification [57]
+}
+
+CertificateInfo ::= BIT STRING {
+
+ reserved(0), -- eUICC has a CERT.EUICC.ECDSA in GlobalPlatform format. The use of this bit is deprecated.
+ certSigningX509(1), -- eUICC has a CERT.EUICC.ECDSA in X.509 format
+ rfu2(2),
+ rfu3(3),
+ reserved2(4), -- Handling of Certificate in GlobalPlatform format. The use of this bit is deprecated.
+ certVerificationX509(5)-- Handling of Certificate in X.509 format
+}
+
+-- Definition of UICCCapability
+UICCCapability ::= BIT STRING {
+/* Sequence is derived from ServicesList[] defined in SIMalliance PEDefinitions*/
+ contactlessSupport(0), -- Contactless (SWP, HCI and associated APIs)
+ usimSupport(1), -- USIM as defined by 3GPP
+ isimSupport(2), -- ISIM as defined by 3GPP
+ csimSupport(3), -- CSIM as defined by 3GPP2
+
+ akaMilenage(4), -- Milenage as AKA algorithm
+ akaCave(5), -- CAVE as authentication algorithm
+ akaTuak128(6), -- TUAK as AKA algorithm with 128 bit key length
+ akaTuak256(7), -- TUAK as AKA algorithm with 256 bit key length
+ rfu1(8), -- reserved for further algorithms
+ rfu2(9), -- reserved for further algorithms
+
+ gbaAuthenUsim(10), -- GBA authentication in the context of USIM
+ gbaAuthenISim(11), -- GBA authentication in the context of ISIM
+ mbmsAuthenUsim(12), -- MBMS authentication in the context of USIM
+ eapClient(13), -- EAP client
+
+ javacard(14), -- Javacard support
+ multos(15), -- Multos support
+
+ multipleUsimSupport(16), -- Multiple USIM applications are supported within the same Profile
+ multipleIsimSupport(17), -- Multiple ISIM applications are supported within the same Profile
+ multipleCsimSupport(18) -- Multiple CSIM applications are supported within the same Profile
+}
+
+-- Definition of DeviceInfo
+DeviceInfo ::= SEQUENCE {
+ tac Octet8,
+ deviceCapabilities DeviceCapabilities,
+ imei Octet8 OPTIONAL
+}
+
+DeviceCapabilities ::= SEQUENCE { -- Highest fully supported release for each definition
+ -- The device SHALL set all the capabilities it supports
+ gsmSupportedRelease VersionType OPTIONAL,
+ utranSupportedRelease VersionType OPTIONAL,
+ cdma2000onexSupportedRelease VersionType OPTIONAL,
+ cdma2000hrpdSupportedRelease VersionType OPTIONAL,
+ cdma2000ehrpdSupportedRelease VersionType OPTIONAL,
+ eutranSupportedRelease VersionType OPTIONAL,
+ contactlessSupportedRelease VersionType OPTIONAL,
+ rspCrlSupportedVersion VersionType OPTIONAL,
+ rspRpmSupportedVersion VersionType OPTIONAL
+}
+
+ProfileInfoListRequest ::= [45] SEQUENCE { -- Tag 'BF2D'
+ searchCriteria [0] CHOICE {
+ isdpAid [APPLICATION 15] OctetTo16, -- AID of the ISD-P, tag '4F'
+ iccid Iccid, -- ICCID, tag '5A'
+ profileClass [21] ProfileClass -- Tag '95'
+ } OPTIONAL,
+ tagList [APPLICATION 28] OCTET STRING OPTIONAL -- tag '5C'
+}
+
+-- Definition of ProfileInfoList
+ProfileInfoListResponse ::= [45] CHOICE { -- Tag 'BF2D'
+ profileInfoListOk SEQUENCE OF ProfileInfo,
+ profileInfoListError ProfileInfoListError
+}
+
+ProfileInfo ::= [PRIVATE 3] SEQUENCE { -- Tag 'E3'
+ iccid Iccid OPTIONAL,
+ isdpAid [APPLICATION 15] OctetTo16 OPTIONAL, -- AID of the ISD-P containing the Profile, tag '4F'
+ profileState [112] ProfileState OPTIONAL, -- Tag '9F70'
+ profileNickname [16] UTF8String (SIZE(0..64)) OPTIONAL, -- Tag '90'
+ serviceProviderName [17] UTF8String (SIZE(0..32)) OPTIONAL, -- Tag '91'
+ profileName [18] UTF8String (SIZE(0..64)) OPTIONAL, -- Tag '92'
+ iconType [19] IconType OPTIONAL, -- Tag '93'
+ icon [20] OCTET STRING (SIZE(0..1024)) OPTIONAL, -- Tag '94', see condition in ES10c:GetProfilesInfo
+ profileClass [21] ProfileClass DEFAULT operational, -- Tag '95'
+ notificationConfigurationInfo [22] SEQUENCE OF NotificationConfigurationInformation OPTIONAL, -- Tag 'B6'
+ profileOwner [23] OperatorID OPTIONAL, -- Tag 'B7'
+ dpProprietaryData [24] DpProprietaryData OPTIONAL, -- Tag 'B8'
+ profilePolicyRules [25] PprIds OPTIONAL -- Tag '99'
+}
+
+PprIds ::= BIT STRING {-- Definition of Profile Policy Rules identifiers
+ pprUpdateControl(0), -- defines how to update PPRs via ES6
+ ppr1(1), -- Indicator for PPR1 'Disabling of this Profile is not allowed'
+ ppr2(2), -- Indicator for PPR2 'Deletion of this Profile is not allowed'
+ ppr3(3) -- Indicator for PPR3 'Deletion of this Profile is required upon its successful disabling'
+}
+
+OperatorID ::= SEQUENCE {
+ mccMnc OCTET STRING (SIZE(3)), -- MCC and MNC coded as defined in 3GPP TS 24.008 [32]
+ gid1 OCTET STRING OPTIONAL, -- referring to content of EF GID1 (file identifier '6F3E') as defined in 3GPP TS 31.102 [54]
+ gid2 OCTET STRING OPTIONAL -- referring to content of EF GID2 (file identifier '6F3F') as defined in 3GPP TS 31.102 [54]
+}
+
+ProfileInfoListError ::= INTEGER {incorrectInputValues(1), undefinedError(127)}
+
+-- Definition of StoreMetadata request
+
+StoreMetadataRequest ::= [37] SEQUENCE { -- Tag 'BF25'
+ iccid Iccid,
+ serviceProviderName [17] UTF8String (SIZE(0..32)), -- Tag '91'
+ profileName [18] UTF8String (SIZE(0..64)), -- Tag '92' (corresponds to 'Short Description' defined in SGP.21 [2])
+ iconType [19] IconType OPTIONAL, -- Tag '93' (JPG or PNG)
+ icon [20] OCTET STRING (SIZE(0..1024)) OPTIONAL, -- Tag '94'(Data of the icon. Size 64 x 64 pixel. This field SHALL only be present if iconType is present)
+ profileClass [21] ProfileClass OPTIONAL, -- Tag '95' (default if absent: 'operational')
+ notificationConfigurationInfo [22] SEQUENCE OF NotificationConfigurationInformation OPTIONAL,
+ profileOwner [23] OperatorID OPTIONAL, -- Tag 'B7'
+ profilePolicyRules [25] PprIds OPTIONAL -- Tag '99'
+}
+
+NotificationEvent ::= BIT STRING {
+ notificationInstall (0),
+ notificationEnable(1),
+ notificationDisable(2),
+ notificationDelete(3)
+}
+
+NotificationConfigurationInformation ::= SEQUENCE {
+ profileManagementOperation NotificationEvent,
+ notificationAddress UTF8String -- FQDN to forward the notification
+}
+
+IconType ::= INTEGER {jpg(0), png(1)}
+ProfileState ::= INTEGER {disabled(0), enabled(1)}
+ProfileClass ::= INTEGER {test(0), provisioning(1), operational(2)}
+
+-- Definition of UpdateMetadata request
+UpdateMetadataRequest ::= [42] SEQUENCE { -- Tag 'BF2A'
+ serviceProviderName [17] UTF8String (SIZE(0..32)) OPTIONAL, -- Tag '91'
+ profileName [18] UTF8String (SIZE(0..64)) OPTIONAL, -- Tag '92'
+ iconType [19] IconType OPTIONAL, -- Tag '93'
+ icon [20] OCTET STRING (SIZE(0..1024)) OPTIONAL, -- Tag '94'
+ profilePolicyRules [25] PprIds OPTIONAL -- Tag '99'
+}
+
+-- Definition of data objects for command PrepareDownload -------------------------
+PrepareDownloadRequest ::= [33] SEQUENCE { -- Tag 'BF21'
+ smdpSigned2 SmdpSigned2, -- Signed information
+ smdpSignature2 [APPLICATION 55] OCTET STRING, -- DP_Sign1, tag '5F37'
+ hashCc Octet32 OPTIONAL, -- Hash of confirmation code
+ smdpCertificate Certificate -- CERT.DPpb.ECDSA
+}
+
+SmdpSigned2 ::= SEQUENCE {
+ transactionId [0] TransactionId, -- The TransactionID generated by the SM DP+
+ ccRequiredFlag BOOLEAN, --Indicates if the Confirmation Code is required
+ bppEuiccOtpk [APPLICATION 73] OCTET STRING OPTIONAL -- otPK.EUICC.ECKA already used for binding the BPP, tag '5F49'
+}
+
+PrepareDownloadResponse ::= [33] CHOICE { -- Tag 'BF21'
+ downloadResponseOk PrepareDownloadResponseOk,
+ downloadResponseError PrepareDownloadResponseError
+}
+
+PrepareDownloadResponseOk ::= SEQUENCE {
+ euiccSigned2 EUICCSigned2, -- Signed information
+ euiccSignature2 [APPLICATION 55] OCTET STRING -- tag '5F37'
+}
+
+EUICCSigned2 ::= SEQUENCE {
+ transactionId [0] TransactionId,
+ euiccOtpk [APPLICATION 73] OCTET STRING, -- otPK.EUICC.ECKA, tag '5F49'
+ hashCc Octet32 OPTIONAL -- Hash of confirmation code
+}
+
+PrepareDownloadResponseError ::= SEQUENCE {
+ transactionId [0] TransactionId,
+ downloadErrorCode DownloadErrorCode
+}
+
+DownloadErrorCode ::= INTEGER {invalidCertificate(1), invalidSignature(2), unsupportedCurve(3), noSessionContext(4), invalidTransactionId(5), undefinedError(127)}
+
+-- Definition of data objects for command AuthenticateServer--------------------
+AuthenticateServerRequest ::= [56] SEQUENCE { -- Tag 'BF38'
+ serverSigned1 ServerSigned1, -- Signed information
+ serverSignature1 [APPLICATION 55] OCTET STRING, -- tag ?5F37?
+ euiccCiPKIdToBeUsed SubjectKeyIdentifier, -- CI Public Key Identifier to be used
+ serverCertificate Certificate, -- RSP Server Certificate CERT.XXauth.ECDSA
+ ctxParams1 CtxParams1
+}
+
+ServerSigned1 ::= SEQUENCE {
+ transactionId [0] TransactionId, -- The Transaction ID generated by the RSP Server
+ euiccChallenge [1] Octet16, -- The eUICC Challenge
+ serverAddress [3] UTF8String, -- The RSP Server address
+ serverChallenge [4] Octet16 -- The RSP Server Challenge
+}
+
+CtxParams1 ::= CHOICE {
+ ctxParamsForCommonAuthentication CtxParamsForCommonAuthentication -- New contextual data objects may be defined for extensibility
+}
+
+CtxParamsForCommonAuthentication ::= SEQUENCE {
+ matchingId UTF8String OPTIONAL,-- The MatchingId could be the Activation code token or EventID or empty
+ deviceInfo DeviceInfo -- The Device information
+}
+
+AuthenticateServerResponse ::= [56] CHOICE { -- Tag 'BF38'
+ authenticateResponseOk AuthenticateResponseOk,
+ authenticateResponseError AuthenticateResponseError
+}
+
+AuthenticateResponseOk ::= SEQUENCE {
+ euiccSigned1 EuiccSigned1, -- Signed information
+ euiccSignature1 [APPLICATION 55] OCTET STRING, --EUICC_Sign1, tag 5F37
+ euiccCertificate Certificate, -- eUICC Certificate (CERT.EUICC.ECDSA) signed by the EUM
+ eumCertificate Certificate -- EUM Certificate (CERT.EUM.ECDSA) signed by the requested CI
+}
+
+EuiccSigned1 ::= SEQUENCE {
+ transactionId [0] TransactionId,
+ serverAddress [3] UTF8String,
+ serverChallenge [4] Octet16, -- The RSP Server Challenge
+ euiccInfo2 [34] EUICCInfo2,
+ ctxParams1 CtxParams1
+}
+
+AuthenticateResponseError ::= SEQUENCE {
+ transactionId [0] TransactionId,
+ authenticateErrorCode AuthenticateErrorCode
+}
+
+AuthenticateErrorCode ::= INTEGER {invalidCertificate(1), invalidSignature(2), unsupportedCurve(3), noSessionContext(4), invalidOid(5), euiccChallengeMismatch(6), ciPKUnknown(7), undefinedError(127)}
+
+-- Definition of Cancel Session------------------------------
+CancelSessionRequest ::= [65] SEQUENCE { -- Tag 'BF41'
+ transactionId TransactionId, -- The TransactionID generated by the RSP Server
+ reason CancelSessionReason
+}
+
+CancelSessionReason ::= INTEGER {endUserRejection(0), postponed(1), timeout(2), pprNotAllowed(3)}
+
+CancelSessionResponse ::= [65] CHOICE { -- Tag 'BF41'
+ cancelSessionResponseOk CancelSessionResponseOk,
+ cancelSessionResponseError INTEGER {invalidTransactionId(5), undefinedError(127)}
+}
+
+CancelSessionResponseOk ::= SEQUENCE {
+ euiccCancelSessionSigned EuiccCancelSessionSigned, -- Signed information
+ euiccCancelSessionSignature [APPLICATION 55] OCTET STRING -- tag '5F37
+}
+
+EuiccCancelSessionSigned ::= SEQUENCE {
+ transactionId TransactionId,
+ smdpOid OBJECT IDENTIFIER, -- SM-DP+ OID as contained in CERT.DPauth.ECDSA
+ reason CancelSessionReason
+}
+
+-- Definition of Bound Profile Package --------------------------
+BoundProfilePackage ::= [54] SEQUENCE { -- Tag 'BF36'
+ initialiseSecureChannelRequest [35] InitialiseSecureChannelRequest, -- Tag 'BF23'
+ firstSequenceOf87 [0] SEQUENCE OF [7] OCTET STRING, -- sequence of '87' TLVs
+ sequenceOf88 [1] SEQUENCE OF [8] OCTET STRING, -- sequence of '88' TLVs
+ secondSequenceOf87 [2] SEQUENCE OF [7] OCTET STRING OPTIONAL, -- sequence of '87' TLVs
+ sequenceOf86 [3] SEQUENCE OF [6] OCTET STRING -- sequence of '86' TLVs
+}
+
+-- Definition of Get eUICC Challenge --------------------------
+GetEuiccChallengeRequest ::= [46] SEQUENCE { -- Tag 'BF2E'
+}
+
+GetEuiccChallengeResponse ::= [46] SEQUENCE { -- Tag 'BF2E'
+ euiccChallenge Octet16 -- random eUICC challenge
+}
+
+-- Definition of Profile Installation Resulceipt
+ProfileInstallationResult ::= [55] SEQUENCE { -- Tag 'BF37'
+ profileInstallationResultData [39] ProfileInstallationResultData,
+ euiccSignPIR EuiccSignPIR
+}
+
+ProfileInstallationResultData ::= [39] SEQUENCE { -- Tag 'BF27'
+ transactionId[0] TransactionId, -- The TransactionID generated by the SM-DP+
+ notificationMetadata[47] NotificationMetadata,
+ smdpOid OBJECT IDENTIFIER OPTIONAL, -- SM-DP+ OID (same value as in CERT.DPpb.ECDSA)
+ finalResult [2] CHOICE {
+ successResult SuccessResult,
+ errorResult ErrorResult
+ }
+}
+
+EuiccSignPIR ::= [APPLICATION 55] OCTET STRING -- Tag '5F37', eUICC?s signature
+
+SuccessResult ::= SEQUENCE {
+ aid [APPLICATION 15] OCTET STRING (SIZE (5..16)), -- AID of ISD-P
+ simaResponse OCTET STRING -- contains (multiple) 'EUICCResponse' as defined in [5]
+}
+
+ErrorResult ::= SEQUENCE {
+ bppCommandId BppCommandId,
+ errorReason ErrorReason,
+ simaResponse OCTET STRING OPTIONAL -- contains (multiple) 'EUICCResponse' as defined in [5]
+}
+
+BppCommandId ::= INTEGER {initialiseSecureChannel(0), configureISDP(1), storeMetadata(2), storeMetadata2(3), replaceSessionKeys(4), loadProfileElements(5)}
+
+ErrorReason ::= INTEGER {
+ incorrectInputValues(1),
+ invalidSignature(2),
+ invalidTransactionId(3),
+ unsupportedCrtValues(4),
+ unsupportedRemoteOperationType(5),
+ unsupportedProfileClass(6),
+ scp03tStructureError(7),
+ scp03tSecurityError(8),
+ installFailedDueToIccidAlreadyExistsOnEuicc(9), installFailedDueToInsufficientMemoryForProfile(10),
+ installFailedDueToInterruption(11),
+ installFailedDueToPEProcessingError (12),
+ installFailedDueToIccidMismatch(13),
+ testProfileInstallFailedDueToInvalidNaaKey(14),
+ pprNotAllowed(15),
+ installFailedDueToUnknownError(127)
+}
+
+ListNotificationRequest ::= [40] SEQUENCE { -- Tag 'BF28'
+ profileManagementOperation [1] NotificationEvent OPTIONAL
+}
+
+ListNotificationResponse ::= [40] CHOICE { -- Tag 'BF28'
+ notificationMetadataList SEQUENCE OF NotificationMetadata,
+ listNotificationsResultError INTEGER {undefinedError(127)}
+}
+
+NotificationMetadata ::= [47] SEQUENCE { -- Tag 'BF2F'
+ seqNumber [0] INTEGER,
+ profileManagementOperation [1] NotificationEvent, --Only one bit set to 1
+ notificationAddress UTF8String, -- FQDN to forward the notification
+ iccid Iccid OPTIONAL
+}
+
+-- Definition of Profile Nickname Information
+SetNicknameRequest ::= [41] SEQUENCE { -- Tag 'BF29'
+ iccid Iccid,
+ profileNickname [16] UTF8String (SIZE(0..64))
+}
+
+SetNicknameResponse ::= [41] SEQUENCE { -- Tag 'BF29'
+ setNicknameResult INTEGER {ok(0), iccidNotFound (1), undefinedError(127)}
+}
+
+id-rsp-cert-objects OBJECT IDENTIFIER ::= { id-rsp cert-objects(2)}
+
+id-rspExt OBJECT IDENTIFIER ::= {id-rsp-cert-objects 0}
+
+id-rspRole OBJECT IDENTIFIER ::= {id-rsp-cert-objects 1}
+
+-- Definition of OIDs for role identification
+id-rspRole-ci OBJECT IDENTIFIER ::= {id-rspRole 0}
+id-rspRole-euicc OBJECT IDENTIFIER ::= {id-rspRole 1}
+id-rspRole-eum OBJECT IDENTIFIER ::= {id-rspRole 2}
+id-rspRole-dp-tls OBJECT IDENTIFIER ::= {id-rspRole 3}
+id-rspRole-dp-auth OBJECT IDENTIFIER ::= {id-rspRole 4}
+id-rspRole-dp-pb OBJECT IDENTIFIER ::= {id-rspRole 5}
+id-rspRole-ds-tls OBJECT IDENTIFIER ::= {id-rspRole 6}
+id-rspRole-ds-auth OBJECT IDENTIFIER ::= {id-rspRole 7}
+
+--Definition of data objects for InitialiseSecureChannel Request
+InitialiseSecureChannelRequest ::= [35] SEQUENCE { -- Tag 'BF23'
+ remoteOpId RemoteOpId, -- Remote Operation Type Identifier (value SHALL be set to installBoundProfilePackage)
+ transactionId [0] TransactionId, -- The TransactionID generated by the SM-DP+
+ controlRefTemplate[6] IMPLICIT ControlRefTemplate, -- Control Reference Template (Key Agreement). Current specification considers a subset of CRT specified in GlobalPlatform Card Specification [8], section 6.4.2.3 for the Mutual Authentication Data Field
+ smdpOtpk [APPLICATION 73] OCTET STRING, ---otPK.DP.ECKA as specified in GlobalPlatform Card Specification [8] section 6.4.2.3 for ePK.OCE.ECKA, tag '5F49'
+ smdpSign [APPLICATION 55] OCTET STRING -- SM-DP's signature, tag '5F37'
+}
+
+ControlRefTemplate ::= SEQUENCE {
+keyType[0] Octet1, -- Key type according to GlobalPlatform Card Specification [8] Table 11-16, AES= '88', Tag '80'
+keyLen[1] Octet1, --Key length in number of bytes. For current specification key length SHALL by 0x10 bytes, Tag '81'
+hostId[4] OctetTo16 -- Host ID value , Tag '84'
+}
+
+--Definition of data objects for ConfigureISDPRequest
+ConfigureISDPRequest ::= [36] SEQUENCE { -- Tag 'BF24'
+ dpProprietaryData [24] DpProprietaryData OPTIONAL -- Tag 'B8'
+}
+
+DpProprietaryData ::= SEQUENCE { -- maximum size including tag and length field: 128 bytes
+ dpOid OBJECT IDENTIFIER -- OID in the tree of the SM-DP+ that created the Profile
+ -- additional data objects defined by the SM-DP+ MAY follow
+}
+
+-- Definition of request message for command ReplaceSessionKeys
+ReplaceSessionKeysRequest ::= [38] SEQUENCE { -- tag 'BF26'
+/*The new initial MAC chaining value*/
+ initialMacChainingValue OCTET STRING,
+/*New session key value for encryption/decryption (PPK-ENC)*/
+ ppkEnc OCTET STRING,
+/*New session key value of the session key C-MAC computation/verification (PPK-MAC)*/
+ ppkCmac OCTET STRING
+}
+
+-- Definition of data objects for RetrieveNotificationsList
+RetrieveNotificationsListRequest ::= [43] SEQUENCE { -- Tag 'BF2B'
+ searchCriteria CHOICE {
+ seqNumber [0] INTEGER,
+ profileManagementOperation [1] NotificationEvent
+ } OPTIONAL
+}
+
+RetrieveNotificationsListResponse ::= [43] CHOICE { -- Tag 'BF2B'
+ notificationList SEQUENCE OF PendingNotification,
+ notificationsListResultError INTEGER {noResultAvailable(1), undefinedError(127)}
+}
+
+PendingNotification ::= CHOICE {
+ profileInstallationResult [55] ProfileInstallationResult, -- tag 'BF37'
+ otherSignedNotification OtherSignedNotification
+}
+
+OtherSignedNotification ::= SEQUENCE {
+ tbsOtherNotification NotificationMetadata,
+ euiccNotificationSignature [APPLICATION 55] OCTET STRING, -- eUICC signature of tbsOtherNotification, Tag '5F37'
+ euiccCertificate Certificate, -- eUICC Certificate (CERT.EUICC.ECDSA) signed by the EUM
+ eumCertificate Certificate -- EUM Certificate (CERT.EUM.ECDSA) signed by the requested CI
+}
+
+-- Definition of notificationSent
+NotificationSentRequest ::= [48] SEQUENCE { -- Tag 'BF30'
+ seqNumber [0] INTEGER
+}
+
+NotificationSentResponse ::= [48] SEQUENCE { -- Tag 'BF30'
+ deleteNotificationStatus INTEGER {ok(0), nothingToDelete(1), undefinedError(127)}
+}
+
+-- Definition of Enable Profile --------------------------
+EnableProfileRequest ::= [49] SEQUENCE { -- Tag 'BF31'
+ profileIdentifier CHOICE {
+ isdpAid [APPLICATION 15] OctetTo16, -- AID, tag '4F'
+ iccid Iccid -- ICCID, tag '5A'
+ },
+ refreshFlag BOOLEAN -- indicating whether REFRESH is required
+}
+
+EnableProfileResponse ::= [49] SEQUENCE { -- Tag 'BF31'
+ enableResult INTEGER {ok(0), iccidOrAidNotFound (1), profileNotInDisabledState(2), disallowedByPolicy(3), wrongProfileReenabling(4), undefinedError(127)}
+}
+
+-- Definition of Disable Profile --------------------------
+DisableProfileRequest ::= [50] SEQUENCE { -- Tag 'BF32'
+ profileIdentifier CHOICE {
+ isdpAid [APPLICATION 15] OctetTo16, -- AID, tag '4F'
+ iccid Iccid -- ICCID, tag '5A'
+ },
+ refreshFlag BOOLEAN -- indicating whether REFRESH is required
+}
+
+DisableProfileResponse ::= [50] SEQUENCE { -- Tag 'BF32'
+ disableResult INTEGER {ok(0), iccidOrAidNotFound (1), profileNotInEnabledState(2), disallowedByPolicy(3), undefinedError(127)}
+}
+
+-- Definition of Delete Profile --------------------------
+DeleteProfileRequest ::= [51] CHOICE { -- Tag 'BF33'
+ isdpAid [APPLICATION 15] OctetTo16, -- AID, tag '4F'
+ iccid Iccid -- ICCID, tag '5A'
+}
+
+DeleteProfileResponse ::= [51] SEQUENCE { -- Tag 'BF33'
+ deleteResult INTEGER {ok(0), iccidOrAidNotFound (1), profileNotInDisabledState(2), disallowedByPolicy(3), undefinedError(127)}
+}
+
+-- Definition of Memory Reset --------------------------
+EuiccMemoryResetRequest ::= [52] SEQUENCE { -- Tag 'BF34'
+ resetOptions [2] BIT STRING {
+ deleteOperationalProfiles(0),
+ deleteFieldLoadedTestProfiles(1),
+ resetDefaultSmdpAddress(2)}
+}
+
+EuiccMemoryResetResponse ::= [52] SEQUENCE { -- Tag 'BF34'
+ resetResult INTEGER {ok(0), nothingToDelete(1), undefinedError(127)}
+}
+
+-- Definition of Get EID --------------------------
+GetEuiccDataRequest ::= [62] SEQUENCE { -- Tag 'BF3E'
+ tagList [APPLICATION 28] Octet1 -- tag '5C', the value SHALL be set to '5A'
+}
+
+GetEuiccDataResponse ::= [62] SEQUENCE { -- Tag 'BF3E'
+ eidValue [APPLICATION 26] Octet16 -- tag '5A'
+}
+
+-- Definition of Get Rat
+
+GetRatRequest ::= [67] SEQUENCE { -- Tag ' BF43'
+ -- No input data
+}
+
+
+GetRatResponse ::= [67] SEQUENCE { -- Tag 'BF43'
+ rat RulesAuthorisationTable
+}
+
+RulesAuthorisationTable ::= SEQUENCE OF ProfilePolicyAuthorisationRule
+ProfilePolicyAuthorisationRule ::= SEQUENCE {
+ pprIds PprIds,
+ allowedOperators SEQUENCE OF OperatorID,
+ pprFlags BIT STRING {consentRequired(0)}
+}
+
+-- Definition of data structure command for loading a CRL
+LoadCRLRequest ::= [53] SEQUENCE { -- Tag 'BF35'
+ -- A CRL-A
+ crl CertificateList
+}
+
+-- Definition of data structure response for loading a CRL
+LoadCRLResponse ::= [53] CHOICE { -- Tag 'BF35'
+loadCRLResponseOk LoadCRLResponseOk,
+loadCRLResponseError LoadCRLResponseError
+}
+
+LoadCRLResponseOk ::= SEQUENCE {
+ missingParts SEQUENCE OF SEQUENCE {
+ number INTEGER (0..MAX)
+ } OPTIONAL
+}
+LoadCRLResponseError ::= INTEGER {invalidSignature(1), invalidCRLFormat(2), notEnoughMemorySpace(3), verificationKeyNotFound(4), undefinedError(127)}
+
+-- Definition of the extension for Certificate Expiration Date
+id-rsp-expDate OBJECT IDENTIFIER ::= {id-rspExt 1}
+ExpirationDate ::= Time
+
+-- Definition of the extension id for total partial-CRL number
+id-rsp-totalPartialCrlNumber OBJECT IDENTIFIER ::= {id-rspExt 2}
+TotalPartialCrlNumber ::= INTEGER
+
+
+-- Definition of the extension id for the partial-CRL number
+id-rsp-partialCrlNumber OBJECT IDENTIFIER ::= {id-rspExt 3}
+PartialCrlNumber ::= INTEGER
+
+-- Definition for ES9+ ASN.1 Binding --------------------------
+RemoteProfileProvisioningRequest ::= [2] CHOICE { -- Tag 'A2'
+ initiateAuthenticationRequest [57] InitiateAuthenticationRequest, -- Tag 'BF39'
+ authenticateClientRequest [59] AuthenticateClientRequest, -- Tag 'BF3B'
+ getBoundProfilePackageRequest [58] GetBoundProfilePackageRequest, -- Tag 'BF3A'
+ cancelSessionRequestEs9 [65] CancelSessionRequestEs9, -- Tag 'BF41'
+ handleNotification [61] HandleNotification -- tag 'BF3D'
+}
+
+RemoteProfileProvisioningResponse ::= [2] CHOICE { -- Tag 'A2'
+ initiateAuthenticationResponse [57] InitiateAuthenticationResponse, -- Tag 'BF39'
+ authenticateClientResponseEs9 [59] AuthenticateClientResponseEs9, -- Tag 'BF3B'
+ getBoundProfilePackageResponse [58] GetBoundProfilePackageResponse, -- Tag 'BF3A'
+ cancelSessionResponseEs9 [65] CancelSessionResponseEs9, -- Tag 'BF41'
+ authenticateClientResponseEs11 [64] AuthenticateClientResponseEs11 -- Tag 'BF40'
+}
+
+InitiateAuthenticationRequest ::= [57] SEQUENCE { -- Tag 'BF39'
+ euiccChallenge [1] Octet16, -- random eUICC challenge
+ smdpAddress [3] UTF8String,
+ euiccInfo1 EUICCInfo1
+}
+
+InitiateAuthenticationResponse ::= [57] CHOICE { -- Tag 'BF39'
+ initiateAuthenticationOk InitiateAuthenticationOkEs9,
+ initiateAuthenticationError INTEGER {
+ invalidDpAddress(1),
+ euiccVersionNotSupportedByDp(2),
+ ciPKNotSupported(3)
+ }
+}
+
+InitiateAuthenticationOkEs9 ::= SEQUENCE {
+ transactionId [0] TransactionId, -- The TransactionID generated by the SM-DP+
+ serverSigned1 ServerSigned1, -- Signed information
+ serverSignature1 [APPLICATION 55] OCTET STRING, -- Server_Sign1, tag '5F37'
+ euiccCiPKIdToBeUsed SubjectKeyIdentifier, -- The curve CI Public Key to be used as required by ES10b.AuthenticateServer
+ serverCertificate Certificate
+}
+
+AuthenticateClientRequest ::= [59] SEQUENCE { -- Tag 'BF3B'
+ transactionId [0] TransactionId,
+ authenticateServerResponse [56] AuthenticateServerResponse -- This is the response from ES10b.AuthenticateServer
+}
+
+AuthenticateClientResponseEs9 ::= [59] CHOICE { -- Tag 'BF3B'
+ authenticateClientOk AuthenticateClientOk,
+ authenticateClientError INTEGER {
+ eumCertificateInvalid(1),
+ eumCertificateExpired(2),
+ euiccCertificateInvalid(3),
+ euiccCertificateExpired(4),
+ euiccSignatureInvalid(5),
+ matchingIdRefused(6),
+ eidMismatch(7),
+ noEligibleProfile(8),
+ ciPKUnknown(9),
+ invalidTransactionId(10),
+ undefinedError(127)
+ }
+}
+
+AuthenticateClientOk ::= SEQUENCE {
+ transactionId [0] TransactionId,
+ profileMetaData [37] StoreMetadataRequest,
+ prepareDownloadRequest [33] PrepareDownloadRequest
+}
+
+GetBoundProfilePackageRequest ::= [58] SEQUENCE { -- Tag 'BF3A'
+ transactionId [0] TransactionId,
+ prepareDownloadResponse [33] PrepareDownloadResponse
+}
+
+GetBoundProfilePackageResponse ::= [58] CHOICE { -- Tag 'BF3A'
+ getBoundProfilePackageOk GetBoundProfilePackageOk,
+ getBoundProfilePackageError INTEGER {
+ euiccSignatureInvalid(1),
+ confirmationCodeMissing(2),
+ confirmationCodeRefused(3),
+ confirmationCodeRetriesExceeded(4),
+ invalidTransactionId(95),
+ undefinedError(127)
+ }
+}
+
+GetBoundProfilePackageOk ::= SEQUENCE {
+ transactionId [0] TransactionId,
+ boundProfilePackage [54] BoundProfilePackage
+}
+
+HandleNotification ::= [61] SEQUENCE { -- Tag 'BF3D'
+ pendingNotification PendingNotification
+}
+
+CancelSessionRequestEs9 ::= [65] SEQUENCE { -- Tag 'BF41'
+ transactionId TransactionId,
+ cancelSessionResponse CancelSessionResponse -- data structure defined for ES10b.CancelSession function
+}
+
+CancelSessionResponseEs9 ::= [65] CHOICE { -- Tag 'BF41'
+ cancelSessionOk CancelSessionOk,
+ cancelSessionError INTEGER {
+ invalidTransactionId(1),
+ euiccSignatureInvalid(2),
+ undefinedError(127)
+ }
+}
+
+CancelSessionOk ::= SEQUENCE { -- This function has no output data
+}
+
+EuiccConfiguredAddressesRequest ::= [60] SEQUENCE { -- Tag 'BF3C'
+}
+
+EuiccConfiguredAddressesResponse ::= [60] SEQUENCE { -- Tag 'BF3C'
+ defaultDpAddress UTF8String OPTIONAL, -- Default SM-DP+ address as an FQDN
+ rootDsAddress UTF8String -- Root SM-DS address as an FQDN
+}
+
+ISDRProprietaryApplicationTemplate ::= [PRIVATE 0] SEQUENCE { -- Tag 'E0'
+ svn [2] VersionType, -- GSMA SGP.22 version supported (SVN)
+ lpaeSupport BIT STRING {
+ lpaeUsingCat(0), -- LPA in the eUICC using Card Application Toolkit
+ lpaeUsingScws(1) -- LPA in the eUICC using Smartcard Web Server
+ } OPTIONAL
+}
+
+LpaeActivationRequest ::= [66] SEQUENCE { -- Tag 'BF42'
+ lpaeOption BIT STRING {
+ activateCatBasedLpae(0), -- LPAe with LUIe based on CAT
+ activateScwsBasedLpae(1) -- LPAe with LUIe based on SCWS
+ }
+}
+
+LpaeActivationResponse ::= [66] SEQUENCE { -- Tag 'BF42'
+ lpaeActivationResult INTEGER {ok(0), notSupported(1)}
+}
+
+SetDefaultDpAddressRequest ::= [63] SEQUENCE { -- Tag 'BF3F'
+ defaultDpAddress UTF8String -- Default SM-DP+ address as an FQDN
+}
+
+SetDefaultDpAddressResponse ::= [63] SEQUENCE { -- Tag 'BF3F'
+ setDefaultDpAddressResult INTEGER { ok (0), undefinedError (127)}
+}
+
+AuthenticateClientResponseEs11 ::= [64] CHOICE { -- Tag 'BF40'
+ authenticateClientOk AuthenticateClientOkEs11,
+ authenticateClientError INTEGER {
+ eumCertificateInvalid(1),
+ eumCertificateExpired(2),
+ euiccCertificateInvalid(3),
+ euiccCertificateExpired(4),
+ euiccSignatureInvalid(5),
+ eventIdUnknown(6),
+ invalidTransactionId(7),
+ undefinedError(127)
+ }
+}
+
+AuthenticateClientOkEs11 ::= SEQUENCE {
+ transactionId TransactionId,
+ eventEntries SEQUENCE OF EventEntries
+}
+
+EventEntries ::= SEQUENCE {
+ eventId UTF8String,
+ rspServerAddress UTF8String
+}
+
+END \ No newline at end of file
diff --git a/pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn b/pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
new file mode 100644
index 0000000..f903ae8
--- /dev/null
+++ b/pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
@@ -0,0 +1,1126 @@
+PEDefinitions {joint-iso-itu-t(2) international-organizations(23) tca(143) euicc-profile(1) spec-version(1) version-three(3)}
+DEFINITIONS
+AUTOMATIC TAGS
+EXTENSIBILITY IMPLIED ::=
+BEGIN
+EXPORTS UICCCapability; -- Definition to be used in remote provisioning specifications for eligibility check
+
+-- Basic integer types, for size constraints
+maxUInt8 INTEGER ::= 255
+UInt8 ::= INTEGER (0..maxUInt8)
+maxUInt15 INTEGER ::= 32767
+UInt15 ::= INTEGER (0..maxUInt15)
+maxUInt16 INTEGER ::= 65535
+UInt16 ::= INTEGER (0..maxUInt16)
+maxUInt31 INTEGER ::= 2147483647
+UInt31 ::= INTEGER (0..maxUInt31)
+
+ApplicationIdentifier ::= OCTET STRING (SIZE(5..16))
+
+PEHeader ::= SEQUENCE {
+mandated NULL OPTIONAL,
+-- if set, indicate that the support of this PE is mandatory
+identification UInt15 -- Identification number of this PE
+}
+
+ProfileElement ::= CHOICE {
+ header ProfileHeader,
+
+/* PEs */
+ genericFileManagement PE-GenericFileManagement,
+ pinCodes PE-PINCodes,
+ pukCodes PE-PUKCodes,
+ akaParameter PE-AKAParameter,
+ cdmaParameter PE-CDMAParameter,
+ securityDomain PE-SecurityDomain,
+ rfm PE-RFM,
+ application PE-Application,
+ nonStandard PE-NonStandard,
+ end PE-End,
+ rfu1 PE-Dummy, -- this avoids renumbering of tag values
+ rfu2 PE-Dummy, -- in case other non-file-system PEs are
+ rfu3 PE-Dummy, -- added here in future versions
+ rfu4 PE-Dummy,
+ rfu5 PE-Dummy,
+
+/* PEs related to file system creation using templates defined in this specification */
+ mf PE-MF,
+ cd PE-CD,
+ telecom PE-TELECOM,
+ usim PE-USIM,
+ opt-usim PE-OPT-USIM,
+ isim PE-ISIM,
+ opt-isim PE-OPT-ISIM,
+ phonebook PE-PHONEBOOK,
+ gsm-access PE-GSM-ACCESS,
+ csim PE-CSIM,
+ opt-csim PE-OPT-CSIM,
+ eap PE-EAP,
+ df-5gs PE-DF-5GS,
+ df-saip PE-DF-SAIP,
+ df-snpn PE-DF-SNPN,
+ df-5gprose PE-DF-5GPROSE,
+ iot PE-IoT,
+ opt-iot PE-OPT-IoT,
+...
+}
+
+PE-Dummy ::= SEQUENCE {
+}
+
+ProfileHeader ::= SEQUENCE {
+major-version UInt8, -- set to 3 for this version of the specification
+minor-version UInt8, -- set to 3 for this version of the specification
+profileType UTF8String (SIZE (1..100)) OPTIONAL, -- Profile type
+iccid OCTET STRING (SIZE (10)), -- ICCID of the Profile
+pol OCTET STRING OPTIONAL,
+eUICC-Mandatory-services ServicesList,
+eUICC-Mandatory-GFSTEList SEQUENCE OF OBJECT IDENTIFIER,
+connectivityParameters OCTET STRING OPTIONAL,
+eUICC-Mandatory-AIDs SEQUENCE OF SEQUENCE {
+ aid ApplicationIdentifier,
+ version OCTET STRING (SIZE(2))
+} OPTIONAL,
+iotOptions IotOptions OPTIONAL -- details for IoT Minimal Profile, mandatory for IoT Minimal Profiles
+}
+
+IotOptions ::= SEQUENCE {
+pix OCTET STRING (SIZE (7..11)) -- PIX value to be used for IoT Minimal Profiles
+}
+
+ServicesList ::= SEQUENCE {
+/* Contactless */
+contactless NULL OPTIONAL,
+
+/* NAAs */
+usim NULL OPTIONAL,
+isim NULL OPTIONAL,
+csim NULL OPTIONAL,
+
+/* NAA algorithms */
+milenage NULL OPTIONAL,
+tuak128 NULL OPTIONAL,
+cave NULL OPTIONAL,
+
+/* USIM/ISIM services */
+gba-usim NULL OPTIONAL,
+gba-isim NULL OPTIONAL,
+mbms NULL OPTIONAL,
+
+/* EAP service */
+eap NULL OPTIONAL,
+
+/* Application Runtime environment */
+ javacard NULL OPTIONAL,
+ multos NULL OPTIONAL,
+
+/* NAAs */
+multiple-usim NULL OPTIONAL,
+multiple-isim NULL OPTIONAL,
+multiple-csim NULL OPTIONAL,
+
+/* Additional algorithms */
+tuak256 NULL OPTIONAL,
+usim-test-algorithm NULL OPTIONAL,
+
+/* File type */
+ber-tlv NULL OPTIONAL,
+
+/* Linked files */
+dfLink NULL OPTIONAL,
+
+/* Support of CAT_TP */
+cat-tp NULL OPTIONAL,
+
+/* Support of 5G */
+get-identity NULL OPTIONAL,
+profile-a-x25519 NULL OPTIONAL,
+profile-b-p256 NULL OPTIONAL,
+suciCalculatorApi NULL OPTIONAL,
+
+/* Support of DNS Resolution */
+dns-resolution NULL OPTIONAL,
+
+/* Support of GP Amd F SCP11 */
+scp11ac NULL OPTIONAL,
+scp11c-authorization-mechanism NULL OPTIONAL,
+
+/* Support of S16 mode as defined in GP Amd D and Amd F */
+s16mode NULL OPTIONAL,
+
+/* Support of enhanced AKA algorithm defined in 3GPP */
+eaka NULL OPTIONAL
+}
+
+-- Definition of UICCCapability
+UICCCapability ::= BIT STRING {
+ contactlessSupport(0), -- Contactless (SWP, HCI and associated APIs)
+ usimSupport(1), -- USIM as defined by 3GPP
+ isimSupport(2), -- ISIM as defined by 3GPP
+ csimSupport(3), -- CSIM as defined by 3GPP2
+
+ akaMilenage(4), -- Milenage as AKA algorithm
+ akaCave(5), -- CAVE as authentication algorithm
+ akaTuak128(6), -- TUAK as AKA algorithm with 128 bit key length
+ akaTuak256(7), -- TUAK as AKA algorithm with 256 bit key length
+ usimTestAlgorithm(8), -- USIM test algorithm
+ rfu2(9), -- reserved for further algorithms
+
+ gbaAuthenUsim(10), -- GBA authentication in the context of USIM
+ gbaAuthenISim(11), -- GBA authentication in the context of ISIM
+ mbmsAuthenUsim(12), -- MBMS authentication in the context of USIM
+ eapClient(13), -- EAP client
+
+ javacard(14), -- Java Card(TM) support
+ multos(15), -- Multos support
+
+ multipleUsimSupport(16), -- Multiple USIM applications are supported within the same Profile
+ multipleIsimSupport(17), -- Multiple ISIM applications are supported within the same Profile
+ multipleCsimSupport(18), -- Multiple CSIM applications are supported within the same Profile
+
+ berTlvFileSupport(19), -- BER TLV files
+ dfLinkSupport(20), -- Linked Directory Files
+ catTp(21), -- Support of CAT TP
+ getIdentity(22), -- Support of the GET IDENTITY command as defined in ETSI TS 102 221
+ profile-a-x25519(23), -- Support of ECIES Profile A as defined in 3GPP TS 33.501 [87]
+ profile-b-p256(24), -- Support of ECIES Profile B as defined in 3GPP TS 33.501 [87]
+ suciCalculatorApi(25), -- Support of the associated API for SUCI derivation as defined in 3GPP 31.130 [31.130]
+ dns-resolution(26), -- Support of DNS Resolution as defined by GP Amd B
+ scp11ac(27), -- Support of GP Amd F SCP11 variants a and c
+ scp11c-authorization-mechanism(28), -- Support of SCP11c authorization mechanism (Tag 'BF20')
+ s16mode(29), -- Support of S16 mode as defined in GP Amd D and Amd F
+ eaka(30), -- Support of enhanced AKA algorithm as defined in 3GPP TS [33.102]
+ iotminimal(31) -- Support of IoT Minimal Profile as described in section 7.5
+}
+
+ProprietaryInfo ::= SEQUENCE {
+ specialFileInformation [PRIVATE 0] OCTET STRING (SIZE (1)) DEFAULT '00'H,
+
+ /* fillPattern, repeatPattern
+ only one of the parameters may be present. Coding and rules defined within ETSI TS 102 222 [102 222] apply
+ */
+
+ fillPattern [PRIVATE 1] OCTET STRING (SIZE(1..200)) OPTIONAL,
+ repeatPattern [PRIVATE 2] OCTET STRING (SIZE(1..200)) OPTIONAL,
+ /* Specific parameters for BER-TLV files */
+ /* Shall be encoded on the minimum number of octets possible
+ (i.e. no leading bytes set to '00' are allowed)*/
+ maximumFileSize [6] OCTET STRING OPTIONAL,
+ fileDetails [4] OCTET STRING (SIZE(1)) DEFAULT '01'H
+}
+
+Fcp ::= SEQUENCE {
+ /* The fileDescriptor shall be encoded as defined in
+ ETSI TS 102 222 [102 222]
+ */
+ fileDescriptor [2] OCTET STRING (SIZE(2..4)) OPTIONAL,
+
+ /* fileID
+ For ADFs, the fileID is a temporary value (named temporary file ID
+ in this document) used only during the profile creation. It has to
+ be unique within a profile and is used for referencing files within
+ this ADF using the file path.
+ */
+ fileID [3] OCTET STRING (SIZE(2)) OPTIONAL,
+
+ /* dfName
+ Only applies for ADFs
+ */
+ dfName [4] ApplicationIdentifier OPTIONAL,
+
+ /* lcsi
+ Coding according to ETSI TS 102 222 [102 222]
+ */
+ lcsi [10] OCTET STRING (SIZE (1)) DEFAULT '05'H,
+
+ /* securityAttributesReferenced
+ Either containing EF ARR ID[2] + record number[1] or
+ record number[1] only and EF ARR ID implicitly known from the
+ context: File ID 2F06 is automatically applied for ADFs,
+ the MF and all files directly located under the MF
+ '6F06' for any other files
+ */
+ securityAttributesReferenced [11] OCTET STRING (SIZE (1..3)) OPTIONAL,
+
+ /* efFileSize
+ Mandatory for EF file types
+ Not allowed for DF files and EF link files
+ Shall be encoded on the minimum number of octets possible
+ (i.e. no leading bytes set to '00' are allowed)
+ */
+ efFileSize [0] OCTET STRING OPTIONAL,
+
+ /* pinStatusTemplateDO
+ Not allowed for EF files
+ Mandatory for DF/ADF files
+ */
+ pinStatusTemplateDO [PRIVATE 6] OCTET STRING OPTIONAL,
+
+ /* shortEFID
+ Not allowed for DF files
+ Optional for EF file types / equivalent to ETSI TS 102 222
+ shortEFID not provided: in case of a template file, SFI
+ is set according to Annex A. For files created
+ by using GenericFileManagement, SFI is calculated from FID
+ shortEFID provided with no value: no SFI is supported
+ for this EF
+ shortEFID available with a length of 1 byte:
+ The Short File Identifier is coded from bits b8 to b4.
+ Bits b3,b2,b1 = 000.
+ */
+ shortEFID [8] OCTET STRING (SIZE (0..1)) OPTIONAL,
+
+ /* proprietaryEFInfo
+ Optional for EF file types
+ Not allowed for DF files
+ */
+ proprietaryEFInfo [5] ProprietaryInfo OPTIONAL,
+
+ /* linkPath
+ Specifies the path to the file to which shall be linked,
+ also valid for DFs. Files within ADFs are addressed
+ by the temporary file ID of the respective ADF. For the coding
+ see filePath. In case of a template link file, an empty linkPath indicates that the link file shall be turned into an independent file.
+ */
+ linkPath [PRIVATE 7] OCTET STRING (SIZE (0..8)) OPTIONAL
+}
+
+File ::= SEQUENCE OF CHOICE {
+ doNotCreate NULL, /* Indicates that this file shall not be created by the eUICC even if present in a PE referencing a "Created by Default" template.
+This flag has no effect for the creation of files in the MF and shall not be used for all the files listed in a "Not Created by Default" template*/
+ fileDescriptor Fcp,
+ fillFileOffset UInt16,
+ fillFileContent OCTET STRING
+}
+
+PE-MF ::= SEQUENCE {
+mf-header PEHeader,
+templateID OBJECT IDENTIFIER,
+mf File,
+ef-pl File OPTIONAL,
+ef-iccid File,
+ef-dir File,
+ef-arr File,
+ef-umpc File OPTIONAL
+}
+
+PE-CD ::= SEQUENCE {
+cd-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-cd File,
+ef-launchpad File OPTIONAL,
+ef-icon File OPTIONAL
+}
+
+PE-TELECOM ::= SEQUENCE {
+telecom-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-telecom File,
+ef-arr File OPTIONAL,
+ef-rma File OPTIONAL,
+ef-sume File OPTIONAL,
+ef-ice-dn File OPTIONAL,
+ef-ice-ff File OPTIONAL,
+ef-psismsc File OPTIONAL,
+df-graphics File OPTIONAL,
+ ef-img File OPTIONAL,
+ ef-iidf File OPTIONAL,
+ ef-ice-graphics File OPTIONAL,
+ ef-launch-scws File OPTIONAL,
+ ef-icon File OPTIONAL,
+df-phonebook File OPTIONAL,
+ ef-pbr File OPTIONAL,
+ ef-ext1 File OPTIONAL,
+ ef-aas File OPTIONAL,
+ ef-gas File OPTIONAL,
+ ef-psc File OPTIONAL,
+ ef-cc File OPTIONAL,
+ ef-puid File OPTIONAL,
+ ef-iap File OPTIONAL,
+ ef-adn File OPTIONAL,
+ ef-pbc File OPTIONAL,
+ ef-anr File OPTIONAL,
+ ef-puri File OPTIONAL,
+ ef-email File OPTIONAL,
+ ef-sne File OPTIONAL,
+ ef-uid File OPTIONAL,
+ ef-grp File OPTIONAL,
+ ef-ccp1 File OPTIONAL,
+df-multimedia File OPTIONAL,
+ ef-mml File OPTIONAL,
+ ef-mmdf File OPTIONAL,
+df-mmss File OPTIONAL,
+ ef-mlpl File OPTIONAL,
+ ef-mspl File OPTIONAL,
+ ef-mmssmode File OPTIONAL,
+df-mcs File OPTIONAL,
+ ef-mst File OPTIONAL,
+ ef-mcs-config File OPTIONAL,
+df-v2x File OPTIONAL,
+ ef-vst File OPTIONAL,
+ ef-v2x-config File OPTIONAL,
+ ef-v2xp-pc5 File OPTIONAL,
+ ef-v2xp-Uu File OPTIONAL
+}
+
+PE-USIM ::= SEQUENCE {
+usim-header PEHeader,
+templateID OBJECT IDENTIFIER,
+adf-usim File,
+ef-imsi File,
+ef-arr File,
+ef-keys File OPTIONAL,
+ef-keysPS File OPTIONAL,
+ef-hpplmn File OPTIONAL,
+ef-ust File, /* The content of UST file shall be modified by the eUICC during profile installation according to the functionality supported by the eUICC platform i.e. in the case where a service is not supported (and not indicated as required) the related bit(s) will be set to zero */
+ef-fdn File OPTIONAL,
+ef-sms File OPTIONAL,
+ef-smsp File OPTIONAL,
+ef-smss File OPTIONAL,
+ef-spn File,
+ef-est File,
+ef-start-hfn File OPTIONAL,
+ef-threshold File OPTIONAL,
+ef-psloci File OPTIONAL,
+ef-acc File,
+ef-fplmn File OPTIONAL,
+ef-loci File OPTIONAL,
+ef-ad File OPTIONAL,
+ef-ecc File,
+ef-netpar File OPTIONAL,
+ef-epsloci File OPTIONAL,
+ef-epsnsc File OPTIONAL
+}
+
+PE-OPT-USIM ::= SEQUENCE {
+optusim-header PEHeader,
+templateID OBJECT IDENTIFIER,
+ef-li File OPTIONAL,
+ef-acmax File OPTIONAL,
+ef-acm File OPTIONAL,
+ef-gid1 File OPTIONAL,
+ef-gid2 File OPTIONAL,
+ef-msisdn File OPTIONAL,
+ef-puct File OPTIONAL,
+ef-cbmi File OPTIONAL,
+ef-cbmid File OPTIONAL,
+ef-sdn File OPTIONAL,
+ef-ext2 File OPTIONAL,
+ef-ext3 File OPTIONAL,
+ef-cbmir File OPTIONAL,
+ef-plmnwact File OPTIONAL,
+ef-oplmnwact File OPTIONAL,
+ef-hplmnwact File OPTIONAL,
+ef-dck File OPTIONAL,
+ef-cnl File OPTIONAL,
+ef-smsr File OPTIONAL,
+ef-bdn File OPTIONAL,
+ef-ext5 File OPTIONAL,
+ef-ccp2 File OPTIONAL,
+ef-ext4 File OPTIONAL,
+ef-acl File OPTIONAL,
+ef-cmi File OPTIONAL,
+ef-ici File OPTIONAL,
+ef-oci File OPTIONAL,
+ef-ict File OPTIONAL,
+ef-oct File OPTIONAL,
+ef-vgcs File OPTIONAL,
+ef-vgcss File OPTIONAL,
+ef-vbs File OPTIONAL,
+ef-vbss File OPTIONAL,
+ef-emlpp File OPTIONAL,
+ef-aaem File OPTIONAL,
+ef-hiddenkey File OPTIONAL,
+ef-pnn File OPTIONAL,
+ef-opl File OPTIONAL,
+ef-mbdn File OPTIONAL,
+ef-ext6 File OPTIONAL,
+ef-mbi File OPTIONAL,
+ef-mwis File OPTIONAL,
+ef-cfis File OPTIONAL,
+ef-ext7 File OPTIONAL,
+ef-spdi File OPTIONAL,
+ef-mmsn File OPTIONAL,
+ef-ext8 File OPTIONAL,
+ef-mmsicp File OPTIONAL,
+ef-mmsup File OPTIONAL,
+ef-mmsucp File OPTIONAL,
+ef-nia File OPTIONAL,
+ef-vgcsca File OPTIONAL,
+ef-vbsca File OPTIONAL,
+ef-gbabp File OPTIONAL,
+ef-msk File OPTIONAL,
+ef-muk File OPTIONAL,
+ef-ehplmn File OPTIONAL,
+ef-gbanl File OPTIONAL,
+ef-ehplmnpi File OPTIONAL,
+ef-lrplmnsi File OPTIONAL,
+ef-nafkca File OPTIONAL,
+ef-spni File OPTIONAL,
+ef-pnni File OPTIONAL,
+ef-ncp-ip File OPTIONAL,
+ef-ufc File OPTIONAL,
+ef-nasconfig File OPTIONAL,
+ef-uicciari File OPTIONAL,
+ef-pws File OPTIONAL,
+ef-fdnuri File OPTIONAL,
+ef-bdnuri File OPTIONAL,
+ef-sdnuri File OPTIONAL,
+ef-ial File OPTIONAL, -- This file was known as ef-iwl in Version 3.2 and earlier of this specification
+ef-ips File OPTIONAL,
+ef-ipd File OPTIONAL,
+ef-epdgid File OPTIONAL,
+ef-epdgselection File OPTIONAL,
+ef-epdgidem File OPTIONAL,
+ef-epdgselectionem File OPTIONAL,
+ef-frompreferred File OPTIONAL,
+ef-imsconfigdata File OPTIONAL,
+ef-3gpppsdataoff File OPTIONAL,
+ef-3gpppsdataoffservicelist File OPTIONAL,
+ef-xcapconfigdata File OPTIONAL,
+ef-earfcnlist File OPTIONAL,
+ef-mudmidconfigdata File OPTIONAL,
+ef-eaka File OPTIONAL
+}
+
+PE-PHONEBOOK ::= SEQUENCE {
+phonebook-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-phonebook File,
+ef-pbr File OPTIONAL,
+ef-ext1 File OPTIONAL,
+ef-aas File OPTIONAL,
+ef-gas File OPTIONAL,
+ef-psc File OPTIONAL,
+ef-cc File OPTIONAL,
+ef-puid File OPTIONAL,
+ef-iap File OPTIONAL,
+ef-adn File OPTIONAL,
+ef-pbc File OPTIONAL,
+ef-anr File OPTIONAL,
+ef-puri File OPTIONAL,
+ef-email File OPTIONAL,
+ef-sne File OPTIONAL,
+ef-uid File OPTIONAL,
+ef-grp File OPTIONAL,
+ef-ccp1 File OPTIONAL
+}
+
+PE-GSM-ACCESS ::= SEQUENCE {
+gsm-access-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-gsm-access File,
+ef-kc File OPTIONAL,
+ef-kcgprs File OPTIONAL,
+ef-cpbcch File OPTIONAL,
+ef-invscan File OPTIONAL
+}
+
+PE-DF-5GS ::= SEQUENCE {
+df-5gs-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-df-5gs File,
+ef-5gs3gpploci File OPTIONAL,
+ef-5gsn3gpploci File OPTIONAL,
+ef-5gs3gppnsc File OPTIONAL,
+ef-5gsn3gppnsc File OPTIONAL,
+ef-5gauthkeys File OPTIONAL,
+ef-uac-aic File OPTIONAL,
+ef-suci-calc-info File OPTIONAL,
+ef-opl5g File OPTIONAL,
+ef-supinai File OPTIONAL,
+ef-routing-indicator File OPTIONAL,
+ef-ursp File OPTIONAL,
+ef-tn3gppsnn File OPTIONAL,
+ef-cag File OPTIONAL,
+ef-sor-cmci File OPTIONAL,
+ef-dri File OPTIONAL,
+ef-5gsedrx File OPTIONAL,
+ef-5gnswo-conf File OPTIONAL,
+ef-mchpplmn File OPTIONAL,
+ef-kausf-derivation File OPTIONAL
+}
+
+PE-DF-SAIP ::= SEQUENCE {
+df-saip-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-df-saip File,
+ef-suci-calc-info-usim File OPTIONAL
+}
+
+PE-DF-SNPN ::= SEQUENCE {
+df-snpn-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-df-snpn File,
+ef-pws-snpn File OPTIONAL
+}
+
+PE-DF-5GPROSE ::= SEQUENCE {
+df-5g-prose-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-df-5g-prose File,
+ef-5g-prose-st File OPTIONAL,
+ef-5g-prose-dd File OPTIONAL,
+ef-5g-prose-dc File OPTIONAL,
+ef-5g-prose-u2nru File OPTIONAL,
+ef-5g-prose-ru File OPTIONAL,
+ef-5g-prose-uir File OPTIONAL
+}
+
+PE-ISIM ::= SEQUENCE {
+isim-header PEHeader,
+templateID OBJECT IDENTIFIER,
+adf-isim File,
+ef-impi File,
+ef-impu File,
+ef-domain File,
+ef-ist File, /* The content of IST file shall be modified by the eUICC during profile installation according to the functionality supported by the eUICC platform i.e. in the case where a service is not supported (and not indicated as required) the related bit(s) will be set to zero */
+ef-ad File OPTIONAL,
+ef-arr File
+}
+
+PE-OPT-ISIM ::= SEQUENCE {
+optisim-header PEHeader,
+templateID OBJECT IDENTIFIER,
+ef-pcscf File OPTIONAL,
+ef-sms File OPTIONAL,
+ef-smsp File OPTIONAL,
+ef-smss File OPTIONAL,
+ef-smsr File OPTIONAL,
+ef-gbabp File OPTIONAL,
+ef-gbanl File OPTIONAL,
+ef-nafkca File OPTIONAL,
+ef-uicciari File OPTIONAL,
+ef-frompreferred File OPTIONAL,
+ef-imsconfigdata File OPTIONAL,
+ef-xcapconfigdata File OPTIONAL,
+ef-webrtcuri File OPTIONAL,
+ef-mudmidconfigdata File OPTIONAL
+}
+
+PE-CSIM ::= SEQUENCE {
+csim-header PEHeader,
+templateID OBJECT IDENTIFIER,
+adf-csim File,
+ef-arr File,
+ef-call-count File,
+ef-imsi-m File,
+ef-imsi-t File,
+ef-tmsi File,
+ef-ah File,
+ef-aop File,
+ef-aloc File,
+ef-cdmahome File,
+ef-znregi File,
+ef-snregi File,
+ef-distregi File,
+ef-accolc File,
+ef-term File,
+ef-acp File,
+ef-prl File,
+ef-ruimid File,
+ef-csim-st File,
+ef-spc File,
+ef-otapaspc File,
+ef-namlock File,
+ef-ota File,
+ef-sp File,
+ef-esn-meid-me File,
+ef-li File,
+ef-usgind File,
+ef-ad File,
+ef-max-prl File,
+ef-spcs File,
+ef-mecrp File,
+ef-home-tag File,
+ef-group-tag File,
+ef-specific-tag File,
+ef-call-prompt File
+}
+
+PE-OPT-CSIM ::= SEQUENCE {
+optcsim-header PEHeader,
+templateID OBJECT IDENTIFIER,
+ef-ssci File OPTIONAL,
+ef-fdn File OPTIONAL,
+ef-sms File OPTIONAL,
+ef-smsp File OPTIONAL,
+ef-smss File OPTIONAL,
+ef-ssfc File OPTIONAL,
+ef-spn File OPTIONAL,
+ef-mdn File OPTIONAL,
+ef-ecc File OPTIONAL,
+ef-me3gpdopc File OPTIONAL,
+ef-3gpdopm File OPTIONAL,
+ef-sipcap File OPTIONAL,
+ef-mipcap File OPTIONAL,
+ef-sipupp File OPTIONAL,
+ef-mipupp File OPTIONAL,
+ef-sipsp File OPTIONAL,
+ef-mipsp File OPTIONAL,
+ef-sippapss File OPTIONAL,
+ef-puzl File OPTIONAL,
+ef-maxpuzl File OPTIONAL,
+ef-hrpdcap File OPTIONAL,
+ef-hrpdupp File OPTIONAL,
+ef-csspr File OPTIONAL,
+ef-atc File OPTIONAL,
+ef-eprl File OPTIONAL,
+ef-bcsmscfg File OPTIONAL,
+ef-bcsmspref File OPTIONAL,
+ef-bcsmstable File OPTIONAL,
+ef-bcsmsp File OPTIONAL,
+ef-bakpara File OPTIONAL,
+ef-upbakpara File OPTIONAL,
+ef-mmsn File OPTIONAL,
+ef-ext8 File OPTIONAL,
+ef-mmsicp File OPTIONAL,
+ef-mmsup File OPTIONAL,
+ef-mmsucp File OPTIONAL,
+ef-auth-capability File OPTIONAL,
+ef-3gcik File OPTIONAL,
+ef-dck File OPTIONAL,
+ef-gid1 File OPTIONAL,
+ef-gid2 File OPTIONAL,
+ef-cdmacnl File OPTIONAL,
+ef-sf-euimid File OPTIONAL,
+ef-est File OPTIONAL,
+ef-hidden-key File OPTIONAL,
+ef-lcsver File OPTIONAL,
+ef-lcscp File OPTIONAL,
+ef-sdn File OPTIONAL,
+ef-ext2 File OPTIONAL,
+ef-ext3 File OPTIONAL,
+ef-ici File OPTIONAL,
+ef-oci File OPTIONAL,
+ef-ext5 File OPTIONAL,
+ef-ccp2 File OPTIONAL,
+ef-applabels File OPTIONAL,
+ef-model File OPTIONAL,
+ef-rc File OPTIONAL,
+ef-smscap File OPTIONAL,
+ef-mipflags File OPTIONAL,
+ef-3gpduppext File OPTIONAL,
+ef-ipv6cap File OPTIONAL,
+ef-tcpconfig File OPTIONAL,
+ef-dgc File OPTIONAL,
+ef-wapbrowsercp File OPTIONAL,
+ef-wapbrowserbm File OPTIONAL,
+ef-mmsconfig File OPTIONAL,
+ef-jdl File OPTIONAL
+}
+
+PE-EAP ::= SEQUENCE {
+eap-header PEHeader,
+templateID OBJECT IDENTIFIER,
+df-eap File,
+ef-eapkeys File OPTIONAL,
+ef-eapstatus File,
+ef-puid File OPTIONAL,
+ef-ps File OPTIONAL,
+ef-curid File OPTIONAL,
+ef-reid File OPTIONAL,
+ef-realm File OPTIONAL
+}
+
+PE-IoT ::= SEQUENCE {
+iot-header PEHeader,
+templateID OBJECT IDENTIFIER,
+mf File OPTIONAL,
+ef-pl File OPTIONAL,
+ef-iccid File OPTIONAL,
+ef-dir File OPTIONAL,
+ef-arr File OPTIONAL,
+ef-umpc File OPTIONAL,
+adf-usim File OPTIONAL,
+ef-imsi File,
+ef-arr-usim File OPTIONAL,
+ef-keys File OPTIONAL,
+ef-keysPS File OPTIONAL,
+ef-hpplmn File OPTIONAL,
+ef-ust File OPTIONAL,
+ef-start-hfn File OPTIONAL,
+ef-threshold File OPTIONAL,
+ef-psloci File OPTIONAL,
+ef-acc File,
+ef-fplmn File OPTIONAL,
+ef-loci File OPTIONAL,
+ef-ad File OPTIONAL,
+ef-ecc File OPTIONAL,
+ef-netpar File OPTIONAL
+}
+
+PE-OPT-IoT ::= SEQUENCE {
+optiot-header PEHeader,
+templateID OBJECT IDENTIFIER,
+ef-fdn File OPTIONAL,
+ef-sms File OPTIONAL,
+ef-smsp File OPTIONAL,
+ef-smss File OPTIONAL,
+ef-spn File OPTIONAL,
+ef-est File OPTIONAL,
+ef-oplmnwact File OPTIONAL,
+ef-hplmnwact File OPTIONAL,
+ef-ehplmn File OPTIONAL,
+ef-epsloci File OPTIONAL,
+ef-epsnsc File OPTIONAL,
+df-df-5gs File OPTIONAL,
+ef-5gs3gpploci File OPTIONAL,
+ef-5gsn3gpploci File OPTIONAL,
+ef-5gs3gppnsc File OPTIONAL,
+ef-5gsn3gppnsc File OPTIONAL,
+ef-5gauthkeys File OPTIONAL,
+ef-uac-aic File OPTIONAL,
+ef-suci-calc-info File OPTIONAL,
+ef-opl5g File OPTIONAL,
+ef-supi-nai File OPTIONAL,
+ef-routing-indicator File OPTIONAL,
+ef-ursp File OPTIONAL,
+ef-tn3gppsnn File OPTIONAL,
+df-df-saip File OPTIONAL,
+ef-suci-calc-info-usim File OPTIONAL
+}
+
+/* Create GenericFileManagement
+*/
+PE-GenericFileManagement ::= SEQUENCE {
+ gfm-header PEHeader,
+ fileManagementCMD SEQUENCE (SIZE (1..MAX)) OF FileManagement
+}
+
+FileManagement ::= SEQUENCE (SIZE (1..MAX)) OF CHOICE {
+filePath [0] OCTET STRING (SIZE (0..8)), -- Use Temporary File ID for ADF
+createFCP [APPLICATION 2] Fcp,
+fillFileOffset UInt16,
+fillFileContent [1] OCTET STRING
+}
+
+MappingParameter ::= SEQUENCE {
+mappingOptions OCTET STRING (SIZE(1)),
+ mappingSource ApplicationIdentifier
+}
+
+AlgoParameter ::= SEQUENCE {
+algorithmID INTEGER {
+ milenage(1),
+ tuak(2),
+ usim-test-algorithm(3)
+},
+algorithmOptions OCTET STRING (SIZE(1)),
+key OCTET STRING,
+opc OCTET STRING, /* OPc for Milenage; TOPc for TUAK; ignored in case of usim-test-algorithm */
+
+/* rotationConstants only apply for Milenage; ignored in case of TUAK and usim-test-algorithm */
+ rotationConstants OCTET STRING (SIZE (5)) DEFAULT '4000204060'H,
+
+/* xoringConstants only apply for Milenage; ignored in case of TUAK and usim-test-algorithm */
+xoringConstants OCTET STRING (SIZE (80)) DEFAULT '0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000400000000000000000000000000000008 'H,
+authCounterMax OCTET STRING (SIZE(3)) OPTIONAL, /* ignored in case of usim-test-algorithm */
+
+/* Number of iterations of Keccak-f[1600] (noted: ) permutation as recommended by 3GPP TS 35.231 [TUAK] in section 7.2.
+This parameter only applies for TUAK; ignored otherwise.*/
+numberOfKeccak UInt8 DEFAULT 1
+}
+
+PE-AKAParameter ::= SEQUENCE {
+ aka-header PEHeader,
+algoConfiguration CHOICE {
+ mappingParameter MappingParameter,
+ algoParameter AlgoParameter
+},
+
+sqnOptions OCTET STRING (SIZE(1)) DEFAULT '02'H, /* ignored in case of usim-test-algorithm */
+-- maximum value for sqnDelta and sqnAgeLimit is '07FFFFFFFFFF'H
+sqnDelta OCTET STRING (SIZE(6)) DEFAULT '000010000000'H, /* ignored in case of usim-test-algorithm */
+sqnAgeLimit OCTET STRING (SIZE(6)) DEFAULT '000010000000'H, /* ignored in case of usim-test-algorithm */
+
+-- Sequence numbers do not include the index (IND)
+-- maximum for any values within sqnInit is '07FFFFFFFFFF'H
+sqnInit SEQUENCE (SIZE (32)) OF OCTET STRING (SIZE (6)) DEFAULT '000000000000'H
+}
+
+PE-CDMAParameter ::= SEQUENCE {
+ cdma-header PEHeader,
+
+/* A-Key for CAVE Authentication */
+authenticationKey OCTET STRING (SIZE(8)),
+
+/*
+Optional value for ssd
+Bytes 1..8: value if shared secret data A
+Bytes 9..16: value if shared secret data B
+*/
+ssd OCTET STRING (SIZE (16)) OPTIONAL,
+
+/*
+ Shared Secrets for HRPD access authentication
+ Includes the shared secret data. This field is coded as defined in section 4.5.7.10 HRPD Access Authentication CHAP SS Parameters of [S0016].
+*/
+hrpdAccessAuthenticationData OCTET STRING (SIZE (2..32)) OPTIONAL,
+
+/*
+ Parameters for simple IP authentication are coded as defined in section 4.5.7.7 SimpleIP CHAP SS Parameters of [S0016].
+*/
+simpleIPAuthenticationData OCTET STRING (SIZE (3..483)) OPTIONAL,
+
+/*
+ Parameters for mobile IP authentication are coded as defined in section 4.5.7.8 MobileIP SS Parameters of [S0016].
+*/
+mobileIPAuthenticationData OCTET STRING (SIZE (5..957)) OPTIONAL
+}
+
+PINKeyReferenceValue ::= INTEGER {
+pinAppl1(1), -- PIN global of App 1
+pinAppl2(2), -- PIN global of App 2
+pinAppl3(3), -- PIN global of App 3
+pinAppl4(4), -- PIN global of App 4
+pinAppl5(5), -- PIN global of App 5
+pinAppl6(6), -- PIN global of App 6
+pinAppl7(7), -- PIN global of App 7
+pinAppl8(8), -- PIN global of App 8
+adm1(10), -- Administrative Key 1
+adm2(11), -- Administrative Key 2
+adm3(12), -- Administrative Key 3
+adm4(13), -- Administrative Key 4
+adm5(14), -- Administrative Key 5
+secondPINAppl1(129), -- PIN local of App 1
+secondPINAppl2(130), -- PIN local of App 2
+secondPINAppl3(131), -- PIN local of App 3
+secondPINAppl4(132), -- PIN local of App 4
+secondPINAppl5(133), -- PIN local of App 5
+secondPINAppl6(134), -- PIN local of App 6
+secondPINAppl7(135), -- PIN local of App 7
+secondPINAppl8(136), -- PIN local of App 8
+adm6(138), -- Administrative Key 6
+adm7(139), -- Administrative Key 7
+adm8(140), -- Administrative Key 8
+adm9(141), -- Administrative Key 9
+adm10(142) -- Administrative Key 10
+}
+
+PINConfiguration ::= SEQUENCE {
+/*
+For every value defined in PINKeyReferenceValue only one entry may be included per PE-PINCodes.
+Within the PE-PINCodes sent in the context of the MF only global PIN key references shall be used. For PINs in any ADF/DF only local PINs shall be defined: secondPINAppl1 secondPINAppl8. It is allowed to define the same PINKeyReferenceValue in multiple directories (e.g. secondPINAppl1 may be defined in the ISIM NAA and within the USIM NAA). Provided they are not linked they shall be handled as two independent PIN values which also may reference different PUK references.
+*/
+ keyReference PINKeyReferenceValue,
+ pinValue OCTET STRING (SIZE (8)),
+/*
+In case no unblockingPINReference is set, no PUK applies for the corresponding PIN.
+In case a PUKKeyReferenceValue is defined the related PUKKeyReferenceValue shall exist within the PE-PUKCodes list.
+Any value defined in PUKKeyReferenceValue may be applied for any PINKeyReferenceValue.
+*/
+ unblockingPINReference PUKKeyReferenceValue OPTIONAL,
+ pinAttributes UInt8 DEFAULT 7,
+ maxNumOfAttemps-retryNumLeft UInt8 DEFAULT 51
+/* maxNumOfAttemps-retryNumLeft is encoded as follows: max Number of Attempts is encoded in the high nibble of this value (Bits b8 to b5) and the Number of retry left is encoded in the low nibble of this value (Bits b4 to b1)*/
+}
+
+PE-PINCodes ::= SEQUENCE {
+ pin-Header PEHeader,
+pinCodes CHOICE {
+ pinconfig SEQUENCE (SIZE (1..26))OF PINConfiguration,
+ filePath OCTET STRING (SIZE (0..8)) /* temporary File ID for ADF, coding according to section 8.3.5 */
+}
+/* PIN can be either defined in the current context or shared
+ with another DF/ADF
+ Up to 26 PIN could be defined according to TS 102 221 [102 221]
+*/
+}
+
+PUKKeyReferenceValue ::= INTEGER {
+pukAppl1(1), -- PUK Reference 1
+pukAppl2(2), -- PUK Reference 2
+pukAppl3(3), -- PUK Reference 3
+pukAppl4(4), -- PUK Reference 4
+pukAppl5(5), -- PUK Reference 5
+pukAppl6(6), -- PUK Reference 6
+pukAppl7(7), -- PUK Reference 7
+pukAppl8(8), -- PUK Reference 8
+secondPUKAppl1(129), -- PUK Reference 9
+secondPUKAppl2(130), -- PUK Reference 10
+secondPUKAppl3(131), -- PUK Reference 11
+secondPUKAppl4(132), -- PUK Reference 12
+secondPUKAppl5(133), -- PUK Reference 13
+secondPUKAppl6(134), -- PUK Reference 14
+secondPUKAppl7(135), -- PUK Reference 15
+secondPUKAppl8(136) -- PUK Reference 16
+}
+
+PUKConfiguration ::= SEQUENCE {
+/*
+Any PUKKeyReferenceValue shall only be defined once within PE-PUKCodes.
+*/
+ keyReference PUKKeyReferenceValue,
+ pukValue OCTET STRING (SIZE (8)),
+ maxNumOfAttemps-retryNumLeft UInt8 DEFAULT 170
+/* maxNumOfAttemps-retryNumLeft is encoded as follows: max Number of Attempts is encoded in the high nibble of this value (Bits b8 to b5) and the Number of retry left is encoded in the low nibble of this value (Bits b4 to b1)*/
+}
+
+PE-PUKCodes ::= SEQUENCE {
+ puk-Header PEHeader,
+pukCodes SEQUENCE (SIZE (1..16))OF PUKConfiguration
+}
+
+PE-SecurityDomain ::= SEQUENCE {
+ sd-Header PEHeader,
+instance ApplicationInstance, -- see section 8.7.3
+keyList SEQUENCE (SIZE (1..MAX)) OF KeyObject OPTIONAL, -- see section 8.6.3
+sdPersoData SEQUENCE (SIZE (1..MAX)) OF OCTET STRING OPTIONAL, /* see section 8.6.4 */
+openPersoData SEQUENCE {
+ restrictParameter [PRIVATE 25] OCTET STRING (SIZE(1)) OPTIONAL,
+ contactlessProtocolParameters OCTET STRING OPTIONAL
+} OPTIONAL, /* see section 8.6.6 */
+catTpParameters SEQUENCE
+{ catTpMaxSduSize UInt16,
+ catTpMaxPduSize UInt16
+} OPTIONAL -- see section 8.6.7
+}
+
+KeyObject::= SEQUENCE {
+keyUsageQualifier [21] OCTET STRING (SIZE (1..2)), /* see [GPCS] section 11.1.9 */
+keyAccess [22] OCTET STRING (SIZE (1)) DEFAULT '00'H,
+keyIdentifier [2] OCTET STRING (SIZE (1)),
+keyVersionNumber [3] OCTET STRING (SIZE (1)),
+keyCounterValue [5] OCTET STRING OPTIONAL,
+keyComponents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
+ keyType [0] OCTET STRING,
+ keyData [6] OCTET STRING,
+ macLength[7] UInt8 DEFAULT 8
+ }
+}
+
+PE-Application ::= SEQUENCE {
+ app-Header PEHeader,
+loadBlock ApplicationLoadPackage OPTIONAL,
+instanceList SEQUENCE (SIZE (1..MAX)) OF ApplicationInstance OPTIONAL
+}
+
+ApplicationLoadPackage ::= SEQUENCE {
+loadPackageAID [APPLICATION 15] ApplicationIdentifier,
+securityDomainAID [APPLICATION 15] ApplicationIdentifier OPTIONAL,
+nonVolatileCodeLimitC6 [PRIVATE 6] OCTET STRING OPTIONAL,
+volatileDataLimitC7 [PRIVATE 7] OCTET STRING OPTIONAL,
+nonVolatileDataLimitC8 [PRIVATE 8] OCTET STRING OPTIONAL,
+hashValue [PRIVATE 1] OCTET STRING OPTIONAL,
+loadBlockObject [PRIVATE 4] OCTET STRING
+}
+
+ApplicationInstance ::= SEQUENCE {
+applicationLoadPackageAID [APPLICATION 15] ApplicationIdentifier,
+classAID [APPLICATION 15] ApplicationIdentifier,
+instanceAID [APPLICATION 15] ApplicationIdentifier,
+extraditeSecurityDomainAID [APPLICATION 15] ApplicationIdentifier OPTIONAL,
+applicationPrivileges [2] OCTET STRING,
+lifeCycleState [3] OCTET STRING (SIZE(1)) DEFAULT '07'H,
+/* Coding according to GP Life Cycle State. */
+
+applicationSpecificParametersC9 [PRIVATE 9] OCTET STRING,
+systemSpecificParameters [PRIVATE 15] ApplicationSystemParameters OPTIONAL,
+applicationParameters [PRIVATE 10] UICCApplicationParameters OPTIONAL,
+processData SEQUENCE (SIZE (1..MAX)) OF OCTET STRING OPTIONAL,
+controlReferenceTemplate [16] ControlReferenceTemplate OPTIONAL
+}
+
+ApplicationSystemParameters ::= SEQUENCE{
+volatileMemoryQuotaC7 [PRIVATE 7] OCTET STRING (SIZE (2..4)) OPTIONAL,
+nonVolatileMemoryQuotaC8 [PRIVATE 8] OCTET STRING (SIZE (2..4)) OPTIONAL,
+globalServiceParameters [PRIVATE 11] OCTET STRING OPTIONAL,
+implicitSelectionParameter [PRIVATE 15] OCTET STRING OPTIONAL,
+volatileReservedMemory [PRIVATE 23] OCTET STRING (SIZE (2..4)) OPTIONAL,
+nonVolatileReservedMemory [PRIVATE 24] OCTET STRING (SIZE (2..4)) OPTIONAL,
+ts102226SIMFileAccessToolkitParameter [PRIVATE 10] OCTET STRING OPTIONAL,
+ts102226AdditionalContactlessParameters [0] TS102226AdditionalContactlessParameters OPTIONAL,
+contactlessProtocolParameters [PRIVATE 25] OCTET STRING OPTIONAL, /* Coded according to Contactless Protocol Parameters Structure as defined in GP Amd. C */
+userInteractionContactlessParameters [PRIVATE 26] OCTET STRING OPTIONAL, /* Coded according to User Interaction Parameters Structure as defined in GP Amd. C */
+cumulativeGrantedVolatileMemory [2] OCTET STRING (SIZE (2..4)) OPTIONAL, /*
+Coded according to Contactless Specific Parameters as defined in GP Amd. C */
+
+cumulativeGrantedNonVolatileMemory [3] OCTET STRING (SIZE (2..4)) OPTIONAL /*
+Coded according to Contactless Specific Parameters as defined in GP Amd. C */
+}
+
+UICCApplicationParameters ::= SEQUENCE {
+uiccToolkitApplicationSpecificParametersField [0] OCTET STRING OPTIONAL,
+uiccAccessApplicationSpecificParametersField [1] OCTET STRING OPTIONAL,
+uiccAdministrativeAccessApplicationSpecificParametersField [2] OCTET STRING OPTIONAL
+}
+
+TS102226AdditionalContactlessParameters ::= SEQUENCE{
+protocolParameterData OCTET STRING /* Parameters for contactless applications encoded according to TS 102 226 */
+}
+
+ControlReferenceTemplate ::= SEQUENCE{
+applicationProviderIdentifier [APPLICATION 32] OCTET STRING
+}
+
+
+PE-RFM ::= SEQUENCE {
+rfm-header [0] PEHeader,
+
+ /* instanceAID
+ AID of the RFM instance
+ */
+ instanceAID [APPLICATION 15] ApplicationIdentifier,
+
+ /* securityDomainAID to which the RFM instance is associated
+ */
+ securityDomainAID [APPLICATION 15] ApplicationIdentifier OPTIONAL,
+
+ tarList [0] SEQUENCE (SIZE (1..MAX)) OF OCTET STRING (SIZE(3)) OPTIONAL,
+
+ minimumSecurityLevel [1] OCTET STRING (SIZE (1)),
+
+uiccAccessDomain OCTET STRING,
+ uiccAdminAccessDomain OCTET STRING,
+
+ /*
+ If the following parameter is available the respective ADF shall be the directory selected by default within an RFM script. In case it is not available the MF shall be the default selection.
+ */
+ adfRFMAccess ADFRFMAccess OPTIONAL
+}
+
+ADFRFMAccess ::= SEQUENCE {
+ adfAID ApplicationIdentifier,
+ adfAccessDomain OCTET STRING,
+ adfAdminAccessDomain OCTET STRING
+}
+
+PE-NonStandard ::= SEQUENCE {
+nonStandard-header PEHeader,
+issuerID OBJECT IDENTIFIER,
+content OCTET STRING
+}
+
+PE-End ::= SEQUENCE {
+end-header PEHeader
+}
+
+PEStatus ::= SEQUENCE {
+status INTEGER {
+ok(0), pe-not-supported(1), memory-failure(2),bad-values(3),
+not-enough-memory(4),invalid-request-format(5), invalid-parameter(6),
+runtime-not-supported (7), lib-not-supported (8),
+template-not-supported (9), feature-not-supported (10),
+pin-code-missing (11),
+unsupported-profile-version(31)
+/* ISO 7816 standard status values apply in the range of [24576...28671]
+and [36864...40959] for reporting status values '6xxx'H and '9xxx'H
+proprietary values apply in the range [40960...65535]
+*/
+},
+identification UInt15 OPTIONAL,
+-- Identification number of the PE triggering the error
+additional-information UInt8 OPTIONAL,
+-- Additional information related to the status code
+offset UInt31 OPTIONAL
+-- Position of the part of the PE generating this status code
+}
+
+EUICCResponse ::= SEQUENCE {
+ peStatus SEQUENCE OF PEStatus,
+profileInstallationAborted NULL OPTIONAL,
+statusMessage UTF8String (SIZE (2..64)) OPTIONAL
+}
+
+END
diff --git a/pySim/esim/bsp.py b/pySim/esim/bsp.py
new file mode 100644
index 0000000..81fe092
--- /dev/null
+++ b/pySim/esim/bsp.py
@@ -0,0 +1,296 @@
+# Early proof-of-concept implementation of
+# GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
+# where BPP is the Bound Profile Package. So the full expansion is the
+# "GSMA eSIM Remote SIM Provisioning Bound Profile Packate Protection Protocol"
+#
+# Originally (SGP.22 v2.x) this was called SCP03t, but it has since been
+# renamed to BSP.
+#
+# (C) 2023 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# SGP.22 v3.0 Section 2.5.3:
+# That block of data is split into segments of a maximum size of 1020 bytes (including the tag, length field and MAC).
+
+import abc
+from typing import List
+import logging
+
+# for BSP key derivation
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
+
+from Cryptodome.Cipher import AES
+from Cryptodome.Hash import CMAC
+
+from pySim.utils import bertlv_encode_len, bertlv_parse_one, b2h
+
+# don't log by default
+logger = logging.getLogger(__name__)
+logger.addHandler(logging.NullHandler())
+
+MAX_SEGMENT_SIZE = 1020
+
+class BspAlgo(abc.ABC):
+ blocksize: int
+
+ def _get_padding(self, in_len: int, multiple: int, padding: int = 0) -> bytes:
+ """Return padding bytes towards multiple of N."""
+ if in_len % multiple == 0:
+ return b''
+ pad_cnt = multiple - (in_len % multiple)
+ return bytes([padding]) * pad_cnt
+
+ def _pad_to_multiple(self, indat: bytes, multiple: int, padding: int = 0) -> bytes:
+ """Pad the input data to multiples of 'multiple'."""
+ return indat + self._get_padding(len(indat), multiple, padding)
+
+ def __str__(self):
+ return self.__class__.__name__
+
+class BspAlgoCrypt(BspAlgo, abc.ABC):
+
+ def __init__(self, s_enc: bytes):
+ self.s_enc = s_enc
+ self.block_nr = 1
+
+ def encrypt(self, data:bytes) -> bytes:
+ """Encrypt given input bytes using the key material given in constructor."""
+ padded_data = self._pad_to_multiple(data, self.blocksize)
+ block_nr = self.block_nr
+ ciphertext = self._encrypt(padded_data)
+ logger.debug("encrypt(block_nr=%u, s_enc=%s, plaintext=%s, padded=%s) -> %s",
+ block_nr, b2h(self.s_enc), b2h(data), b2h(padded_data), b2h(ciphertext))
+ return ciphertext
+
+ def decrypt(self, data:bytes) -> bytes:
+ """Decrypt given input bytes using the key material given in constructor."""
+ return self._unpad(self._decrypt(data))
+
+ @abc.abstractmethod
+ def _unpad(self, padded: bytes) -> bytes:
+ """Remove the padding from padded data."""
+
+ @abc.abstractmethod
+ def _encrypt(self, data:bytes) -> bytes:
+ """Actual implementation, to be implemented by derived class."""
+
+ @abc.abstractmethod
+ def _decrypt(self, data:bytes) -> bytes:
+ """Actual implementation, to be implemented by derived class."""
+
+class BspAlgoCryptAES128(BspAlgoCrypt):
+ name = 'AES-CBC-128'
+ blocksize = 16
+
+ def _get_padding(self, in_len: int, multiple: int, padding: int = 0):
+ # SGP.22 section 2.6.4.4
+ # Append a byte with value '80' to the right of the data block;
+ # Append 0 to 15 bytes with value '00' so that the length of the padded data block
+ # is a multiple of 16 bytes.
+ return b'\x80' + super()._get_padding(in_len + 1, multiple, padding)
+
+ def _unpad(self, padded: bytes) -> bytes:
+ """Remove the customary 80 00 00 ... padding used for AES."""
+ # first remove any trailing zero bytes
+ stripped = padded.rstrip(b'\0')
+ # then remove the final 80
+ assert stripped[-1] == 0x80
+ return stripped[:-1]
+
+ def _get_icv(self):
+ # The binary value of this number SHALL be left padded with zeroes to form a full block.
+ data = self.block_nr.to_bytes(self.blocksize, "big")
+ #iv = bytes([0] * (self.blocksize-1)) + b'\x01'
+ iv = bytes([0] * self.blocksize)
+ # This block SHALL be encrypted with S-ENC to produce the ICV for command encryption.
+ cipher = AES.new(self.s_enc, AES.MODE_CBC, iv)
+ icv = cipher.encrypt(data)
+ logger.debug("_get_icv(block_nr=%u, data=%s) -> icv=%s", self.block_nr, b2h(data), b2h(icv))
+ self.block_nr = self.block_nr + 1
+ return icv
+
+ def _encrypt(self, data: bytes) -> bytes:
+ cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv())
+ return cipher.encrypt(data)
+
+ def _decrypt(self, data: bytes) -> bytes:
+ cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv())
+ return cipher.decrypt(data)
+
+
+class BspAlgoMac(BspAlgo, abc.ABC):
+ l_mac = 0 # must be overridden by derived class
+
+ def __init__(self, s_mac: bytes, initial_mac_chaining_value: bytes):
+ self.s_mac = s_mac
+ self.mac_chain = initial_mac_chaining_value
+
+ def auth(self, tag: int, data: bytes) -> bytes:
+ assert tag in range (256)
+ # The input data used for C-MAC computation comprises the MAC Chaining value, the tag, the final length and the result of step 2
+ lcc = len(data) + self.l_mac
+ tag_and_length = bytes([tag]) + bertlv_encode_len(lcc)
+ temp_data = self.mac_chain + tag_and_length + data
+ old_mcv = self.mac_chain
+ c_mac = self._auth(temp_data)
+ # The output data is computed by concatenating the following data: the tag, the final length, the result of step 2 and the C-MAC value.
+ ret = tag_and_length + data + c_mac
+ logger.debug("auth(tag=0x%x, mcv=%s, s_mac=%s, plaintext=%s, temp=%s) -> %s",
+ tag, b2h(old_mcv), b2h(self.s_mac), b2h(data), b2h(temp_data), b2h(ret))
+ return ret
+
+ def verify(self, ciphertext: bytes) -> bool:
+ mac_stripped = ciphertext[0:-self.l_mac]
+ mac_received = ciphertext[-self.l_mac:]
+ temp_data = self.mac_chain + mac_stripped
+ mac_computed = self._auth(temp_data)
+ if mac_received != mac_computed:
+ raise ValueError("MAC value not matching: received: %s, computed: %s" % (mac_received, mac_computed))
+ return mac_stripped
+
+ @abc.abstractmethod
+ def _auth(self, temp_data: bytes) -> bytes:
+ """To be implemented by algorithm specific derived class."""
+
+class BspAlgoMacAES128(BspAlgoMac):
+ name = 'AES-CMAC-128'
+ l_mac = 8
+
+ def _auth(self, temp_data: bytes) -> bytes:
+ # The full MAC value is computed using the MACing algorithm as defined in table 4c.
+ cmac = CMAC.new(self.s_mac, ciphermod=AES)
+ cmac.update(temp_data)
+ full_c_mac = cmac.digest()
+ # Subsequent MAC chaining values are the full result of step 4 of the previous data block
+ self.mac_chain = full_c_mac
+ # If the algorithm is AES-CBC-128 or SM4-CBC, the C-MAC value is the 8 most significant bytes of the result of step 4
+ return full_c_mac[0:8]
+
+
+
+def bsp_key_derivation(shared_secret: bytes, key_type: int, key_length: int, host_id: bytes, eid, l : int = 16):
+ """BSP protocol key derivation as per SGP.22 v3.0 Section 2.6.4.2"""
+ assert key_type <= 255
+ assert key_length <= 255
+
+ host_id_lv = bertlv_encode_len(len(host_id)) + host_id
+ eid_lv = bertlv_encode_len(len(eid)) + eid
+ shared_info = bytes([key_type, key_length]) + host_id_lv + eid_lv
+ logger.debug("kdf_shared_info: %s", b2h(shared_info))
+
+ # X9.63 Key Derivation Function with SHA256
+ xkdf = X963KDF(algorithm=hashes.SHA256(), length=l*3, sharedinfo=shared_info)
+ out = xkdf.derive(shared_secret)
+ logger.debug("kdf_out: %s", b2h(out))
+
+ initial_mac_chaining_value = out[0:l]
+ s_enc = out[l:2*l]
+ s_mac = out[l*2:3*l]
+
+ return s_enc, s_mac, initial_mac_chaining_value
+
+
+
+class BspInstance:
+ """An instance of the BSP crypto. Initialized once with the key material via constructor,
+ then the user can call any number of encrypt_and_mac cycles to protect plaintext and
+ generate the respective ciphertext."""
+ def __init__(self, s_enc: bytes, s_mac: bytes, initial_mcv: bytes):
+ logger.debug("%s(s_enc=%s, s_mac=%s, initial_mcv=%s)", self.__class__.__name__, b2h(s_enc), b2h(s_mac), b2h(initial_mcv))
+ self.c_algo = BspAlgoCryptAES128(s_enc)
+ self.m_algo = BspAlgoMacAES128(s_mac, initial_mcv)
+
+ TAG_LEN = 1
+ length_len = len(bertlv_encode_len(MAX_SEGMENT_SIZE))
+ self.max_payload_size = MAX_SEGMENT_SIZE - TAG_LEN - length_len - self.m_algo.l_mac
+
+ @classmethod
+ def from_kdf(cls, shared_secret: bytes, key_type: int, key_length: int, host_id: bytes, eid: bytes):
+ """Convenience constructor for constructing an instance with keys from KDF."""
+ s_enc, s_mac, initial_mcv = bsp_key_derivation(shared_secret, key_type, key_length, host_id, eid)
+ return cls(s_enc, s_mac, initial_mcv)
+
+ def encrypt_and_mac_one(self, tag: int, plaintext:bytes) -> bytes:
+ """Encrypt + MAC a single plaintext TLV. Returns the protected ciphertex."""
+ assert tag <= 255
+ assert len(plaintext) <= self.max_payload_size
+ logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext))
+ ciphered = self.c_algo.encrypt(plaintext)
+ maced = self.m_algo.auth(tag, ciphered)
+ return maced
+
+ def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
+ remainder = plaintext
+ result = []
+ while len(remainder):
+ remaining_len = len(remainder)
+ if remaining_len < self.max_payload_size:
+ segment_len = remaining_len
+ segment = remainder
+ remainder = b''
+ else:
+ segment_len = self.max_payload_size
+ segment = remainder[0:segment_len]
+ remainder = remainder[segment_len:]
+ result.append(self.encrypt_and_mac_one(tag, segment))
+ return result
+
+ def mac_only_one(self, tag: int, plaintext: bytes) -> bytes:
+ """MAC a single plaintext TLV. Returns the protected ciphertex."""
+ assert tag <= 255
+ assert len(plaintext) < self.max_payload_size
+ maced = self.m_algo.auth(tag, plaintext)
+ # The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
+ self.c_algo.block_nr += 1
+ return maced
+
+ def mac_only(self, tag: int, plaintext:bytes) -> List[bytes]:
+ remainder = plaintext
+ result = []
+ while len(remainder):
+ remaining_len = len(remainder)
+ if remaining_len < self.max_payload_size:
+ segment_len = remaining_len
+ segment = remainder
+ remainder = b''
+ else:
+ segment_len = self.max_payload_size
+ segment = remainder[0:segment_len]
+ remainder = remainder[segment_len:]
+ result.append(self.mac_only_one(tag, segment))
+ return result
+
+ def demac_and_decrypt_one(self, ciphertext: bytes) -> bytes:
+ payload = self.m_algo.verify(ciphertext)
+ tdict, l, val, remain = bertlv_parse_one(payload)
+ logger.debug("tag=%s, l=%u, val=%s, remain=%s", tdict, l, b2h(val), b2h(remain))
+ plaintext = self.c_algo.decrypt(val)
+ return plaintext
+
+ def demac_and_decrypt(self, ciphertext_list: List[bytes]) -> bytes:
+ plaintext_list = [self.demac_and_decrypt_one(x) for x in ciphertext_list]
+ return b''.join(plaintext_list)
+
+ def demac_only_one(self, ciphertext: bytes) -> bytes:
+ payload = self.m_algo.verify(ciphertext)
+ _tdict, _l, val, _remain = bertlv_parse_one(payload)
+ # The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
+ self.c_algo.block_nr += 1
+ return val
+
+ def demac_only(self, ciphertext_list: List[bytes]) -> bytes:
+ plaintext_list = [self.demac_only_one(x) for x in ciphertext_list]
+ return b''.join(plaintext_list)
diff --git a/pySim/esim/es2p.py b/pySim/esim/es2p.py
new file mode 100644
index 0000000..fa21d2c
--- /dev/null
+++ b/pySim/esim/es2p.py
@@ -0,0 +1,462 @@
+"""GSMA eSIM RSP ES2+ interface according to SGP.22 v2.5"""
+
+# (C) 2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import abc
+import requests
+import logging
+import json
+from datetime import datetime
+import time
+import base64
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+class ApiParam(abc.ABC):
+ """A class reprsenting a single parameter in the ES2+ API."""
+ @classmethod
+ def verify_decoded(cls, data):
+ """Verify the decoded reprsentation of a value. Should raise an exception if somthing is odd."""
+ pass
+
+ @classmethod
+ def verify_encoded(cls, data):
+ """Verify the encoded reprsentation of a value. Should raise an exception if somthing is odd."""
+ pass
+
+ @classmethod
+ def encode(cls, data):
+ """[Validate and] Encode the given value."""
+ cls.verify_decoded(data)
+ encoded = cls._encode(data)
+ cls.verify_decoded(encoded)
+ return encoded
+
+ @classmethod
+ def _encode(cls, data):
+ """encoder function, typically [but not always] overridden by derived class."""
+ return data
+
+ @classmethod
+ def decode(cls, data):
+ """[Validate and] Decode the given value."""
+ cls.verify_encoded(data)
+ decoded = cls._decode(data)
+ cls.verify_decoded(decoded)
+ return decoded
+
+ @classmethod
+ def _decode(cls, data):
+ """decoder function, typically [but not always] overridden by derived class."""
+ return data
+
+class ApiParamString(ApiParam):
+ """Base class representing an API parameter of 'string' type."""
+ pass
+
+
+class ApiParamInteger(ApiParam):
+ """Base class representing an API parameter of 'integer' type."""
+ @classmethod
+ def _decode(cls, data):
+ return int(data)
+
+ @classmethod
+ def _encode(cls, data):
+ return str(data)
+
+ @classmethod
+ def verify_decoded(cls, data):
+ if not isinstance(data, int):
+ raise TypeError('Expected an integer input data type')
+
+ @classmethod
+ def verify_encoded(cls, data):
+ if isinstance(data, int):
+ return
+ if not data.isdecimal():
+ raise ValueError('integer (%s) contains non-decimal characters' % data)
+ assert str(int(data)) == data
+
+class ApiParamBoolean(ApiParam):
+ """Base class representing an API parameter of 'boolean' type."""
+ @classmethod
+ def _encode(cls, data):
+ return bool(data)
+
+class ApiParamFqdn(ApiParam):
+ """String, as a list of domain labels concatenated using the full stop (dot, period) character as
+ separator between labels. Labels are restricted to the Alphanumeric mode character set defined in table 5
+ of ISO/IEC 18004"""
+ @classmethod
+ def verify_encoded(cls, data):
+ # FIXME
+ pass
+
+class param:
+ class Iccid(ApiParamString):
+ """String representation of 19 or 20 digits, where the 20th digit MAY optionally be the padding
+ character F."""
+ @classmethod
+ def _encode(cls, data):
+ data = str(data)
+ # SGP.22 version prior to 2.2 do not require support for 19-digit ICCIDs, so let's always
+ # encode it with padding F at the end.
+ if len(data) == 19:
+ data += 'F'
+ return data
+
+ @classmethod
+ def verify_encoded(cls, data):
+ if len(data) not in [19, 20]:
+ raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
+
+ @classmethod
+ def _decode(cls, data):
+ # strip trailing padding (if it's 20 digits)
+ if len(data) == 20 and data[-1] in ['F', 'f']:
+ data = data[:-1]
+ return data
+
+ @classmethod
+ def verify_decoded(cls, data):
+ data = str(data)
+ if len(data) not in [19, 20]:
+ raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
+ if len(data) == 19:
+ decimal_part = data
+ else:
+ decimal_part = data[:-1]
+ final_part = data[-1:]
+ if final_part not in ['F', 'f'] and not final_part.isdecimal():
+ raise ValueError('ICCID (%s) contains non-decimal characters' % data)
+ if not decimal_part.isdecimal():
+ raise ValueError('ICCID (%s) contains non-decimal characters' % data)
+
+
+ class Eid(ApiParamString):
+ """String of 32 decimal characters"""
+ @classmethod
+ def verify_encoded(cls, data):
+ if len(data) != 32:
+ raise ValueError('EID length invalid: "%s" (%u)' % (data, len(data)))
+
+ @classmethod
+ def verify_decoded(cls, data):
+ if not data.isdecimal():
+ raise ValueError('EID (%s) contains non-decimal characters' % data)
+
+ class ProfileType(ApiParamString):
+ pass
+
+ class MatchingId(ApiParamString):
+ pass
+
+ class ConfirmationCode(ApiParamString):
+ pass
+
+ class SmdsAddress(ApiParamFqdn):
+ pass
+
+ class SmdpAddress(ApiParamFqdn):
+ pass
+
+ class ReleaseFlag(ApiParamBoolean):
+ pass
+
+ class FinalProfileStatusIndicator(ApiParamString):
+ pass
+
+ class Timestamp(ApiParamString):
+ """String format as specified by W3C: YYYY-MM-DDThh:mm:ssTZD"""
+ @classmethod
+ def _decode(cls, data):
+ return datetime.fromisoformat(data)
+
+ @classmethod
+ def _encode(cls, data):
+ return datetime.toisoformat(data)
+
+ class NotificationPointId(ApiParamInteger):
+ pass
+
+ class NotificationPointStatus(ApiParam):
+ pass
+
+ class ResultData(ApiParam):
+ @classmethod
+ def _decode(cls, data):
+ return base64.b64decode(data)
+
+ @classmethod
+ def _encode(cls, data):
+ return base64.b64encode(data)
+
+ class JsonResponseHeader(ApiParam):
+ """SGP.22 section 6.5.1.4."""
+ @classmethod
+ def verify_decoded(cls, data):
+ fe_status = data.get('functionExecutionStatus')
+ if not fe_status:
+ raise ValueError('Missing mandatory functionExecutionStatus in header')
+ status = fe_status.get('status')
+ if not status:
+ raise ValueError('Missing mandatory status in header functionExecutionStatus')
+ if status not in ['Executed-Success', 'Executed-WithWarning', 'Failed', 'Expired']:
+ raise ValueError('Unknown/unspecified status "%s"' % status)
+
+
+class HttpStatusError(Exception):
+ pass
+
+class HttpHeaderError(Exception):
+ pass
+
+class Es2PlusApiError(Exception):
+ """Exception representing an error at the ES2+ API level (status != Executed)."""
+ def __init__(self, func_ex_status: dict):
+ self.status = func_ex_status['status']
+ sec = {
+ 'subjectCode': None,
+ 'reasonCode': None,
+ 'subjectIdentifier': None,
+ 'message': None,
+ }
+ actual_sec = func_ex_status.get('statusCodeData', None)
+ sec.update(actual_sec)
+ self.subject_code = sec['subjectCode']
+ self.reason_code = sec['reasonCode']
+ self.subject_id = sec['subjectIdentifier']
+ self.message = sec['message']
+
+ def __str__(self):
+ return f'{self.status}("{self.subject_code}","{self.reason_code}","{self.subject_id}","{self.message}")'
+
+class Es2PlusApiFunction(abc.ABC):
+ """Base classs for representing an ES2+ API Function."""
+ # the below class variables are expected to be overridden in derived classes
+
+ path = None
+ # dictionary of input parameters. key is parameter name, value is ApiParam class
+ input_params = {}
+ # list of mandatory input parameters
+ input_mandatory = []
+ # dictionary of output parameters. key is parameter name, value is ApiParam class
+ output_params = {}
+ # list of mandatory output parameters (for successful response)
+ output_mandatory = []
+ # expected HTTP status code of the response
+ expected_http_status = 200
+ # the HTTP method used (GET, OPTIONS, HEAD, POST, PUT, PATCH or DELETE)
+ http_method = 'POST'
+
+ def __init__(self, url_prefix: str, func_req_id: str, session):
+ self.url_prefix = url_prefix
+ self.func_req_id = func_req_id
+ self.session = session
+
+ def encode(self, data: dict, func_call_id: str) -> dict:
+ """Validate an encode input dict into JSON-serializable dict for request body."""
+ output = {
+ 'header': {
+ 'functionRequesterIdentifier': self.func_req_id,
+ 'functionCallIdentifier': func_call_id
+ }
+ }
+ for p in self.input_mandatory:
+ if not p in data:
+ raise ValueError('Mandatory input parameter %s missing' % p)
+ for p, v in data.items():
+ p_class = self.input_params.get(p)
+ if not p_class:
+ logger.warning('Unexpected/unsupported input parameter %s=%s', p, v)
+ output[p] = v
+ else:
+ output[p] = p_class.encode(v)
+ return output
+
+
+ def decode(self, data: dict) -> dict:
+ """[further] Decode and validate the JSON-Dict of the respnse body."""
+ output = {}
+ # let's first do the header, it's special
+ if not 'header' in data:
+ raise ValueError('Mandatory output parameter "header" missing')
+ hdr_class = self.output_params.get('header')
+ output['header'] = hdr_class.decode(data['header'])
+
+ if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
+ raise Es2PlusApiError(output['header']['functionExecutionStatus'])
+ # we can only expect mandatory parameters to be present in case of successful execution
+ for p in self.output_mandatory:
+ if p == 'header':
+ continue
+ if not p in data:
+ raise ValueError('Mandatory output parameter "%s" missing' % p)
+ for p, v in data.items():
+ p_class = self.output_params.get(p)
+ if not p_class:
+ logger.warning('Unexpected/unsupported output parameter "%s"="%s"', p, v)
+ output[p] = v
+ else:
+ output[p] = p_class.decode(v)
+ return output
+
+ def call(self, data: dict, func_call_id:str, timeout=10) -> dict:
+ """Make an API call to the ES2+ API endpoint represented by this object.
+ Input data is passed in `data` as json-serializable dict. Output data
+ is returned as json-deserialized dict."""
+ url = self.url_prefix + self.path
+ encoded = json.dumps(self.encode(data, func_call_id))
+ headers = {
+ 'Content-Type': 'application/json',
+ 'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
+ }
+
+ logger.debug("HTTP REQ %s - '%s'" % (url, encoded))
+ response = self.session.request(self.http_method, url, data=encoded, headers=headers, timeout=timeout)
+ logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
+ logger.debug("HTTP RSP: %s" % (response.content))
+
+ if response.status_code != self.expected_http_status:
+ raise HttpStatusError(response)
+ if not response.headers.get('Content-Type').startswith(headers['Content-Type']):
+ raise HttpHeaderError(response)
+ if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
+ raise HttpHeaderError(response)
+
+ return self.decode(response.json())
+
+
+# ES2+ DownloadOrder function (SGP.22 section 5.3.1)
+class DownloadOrder(Es2PlusApiFunction):
+ path = '/gsma/rsp2/es2plus/downloadOrder'
+ input_params = {
+ 'eid': param.Eid,
+ 'iccid': param.Iccid,
+ 'profileType': param.ProfileType
+ }
+ output_params = {
+ 'header': param.JsonResponseHeader,
+ 'iccid': param.Iccid,
+ }
+ output_mandatory = ['header', 'iccid']
+
+# ES2+ ConfirmOrder function (SGP.22 section 5.3.2)
+class ConfirmOrder(Es2PlusApiFunction):
+ path = '/gsma/rsp2/es2plus/confirmOrder'
+ input_params = {
+ 'iccid': param.Iccid,
+ 'eid': param.Eid,
+ 'matchingId': param.MatchingId,
+ 'confirmationCode': param.ConfirmationCode,
+ 'smdsAddress': param.SmdsAddress,
+ 'releaseFlag': param.ReleaseFlag,
+ }
+ input_mandatory = ['iccid', 'releaseFlag']
+ output_params = {
+ 'header': param.JsonResponseHeader,
+ 'eid': param.Eid,
+ 'matchingId': param.MatchingId,
+ 'smdpAddress': param.SmdpAddress,
+ }
+ output_mandatory = ['header', 'matchingId']
+
+# ES2+ CancelOrder function (SGP.22 section 5.3.3)
+class CancelOrder(Es2PlusApiFunction):
+ path = '/gsma/rsp2/es2plus/cancelOrder'
+ input_params = {
+ 'iccid': param.Iccid,
+ 'eid': param.Eid,
+ 'matchingId': param.MatchingId,
+ 'finalProfileStatusIndicator': param.FinalProfileStatusIndicator,
+ }
+ input_mandatory = ['finalProfileStatusIndicator', 'iccid']
+ output_params = {
+ 'header': param.JsonResponseHeader,
+ }
+ output_mandatory = ['header']
+
+# ES2+ ReleaseProfile function (SGP.22 section 5.3.4)
+class ReleaseProfile(Es2PlusApiFunction):
+ path = '/gsma/rsp2/es2plus/releaseProfile'
+ input_params = {
+ 'iccid': param.Iccid,
+ }
+ input_mandatory = ['iccid']
+ output_params = {
+ 'header': param.JsonResponseHeader,
+ }
+ output_mandatory = ['header']
+
+# ES2+ HandleDownloadProgress function (SGP.22 section 5.3.5)
+class HandleDownloadProgressInfo(Es2PlusApiFunction):
+ path = '/gsma/rsp2/es2plus/handleDownloadProgressInfo'
+ input_params = {
+ 'eid': param.Eid,
+ 'iccid': param.Iccid,
+ 'profileType': param.ProfileType,
+ 'timestamp': param.Timestamp,
+ 'notificationPointId': param.NotificationPointId,
+ 'notificationPointStatus': param.NotificationPointStatus,
+ 'resultData': param.ResultData,
+ }
+ input_mandatory = ['iccid', 'profileType', 'timestamp', 'notificationPointId', 'notificationPointStatus']
+ expected_http_status = 204
+
+
+class Es2pApiClient:
+ """Main class representing a full ES2+ API client. Has one method for each API function."""
+ def __init__(self, url_prefix:str, func_req_id:str, server_cert_verify: str = None, client_cert: str = None):
+ self.func_id = 0
+ self.session = requests.Session()
+ if server_cert_verify:
+ self.session.verify = server_cert_verify
+ if client_cert:
+ self.session.cert = client_cert
+
+ self.downloadOrder = DownloadOrder(url_prefix, func_req_id, self.session)
+ self.confirmOrder = ConfirmOrder(url_prefix, func_req_id, self.session)
+ self.cancelOrder = CancelOrder(url_prefix, func_req_id, self.session)
+ self.releaseProfile = ReleaseProfile(url_prefix, func_req_id, self.session)
+ self.handleDownloadProgressInfo = HandleDownloadProgressInfo(url_prefix, func_req_id, self.session)
+
+ def _gen_func_id(self) -> str:
+ """Generate the next function call id."""
+ self.func_id += 1
+ return 'FCI-%u-%u' % (time.time(), self.func_id)
+
+
+ def call_downloadOrder(self, data: dict) -> dict:
+ """Perform ES2+ DownloadOrder function (SGP.22 section 5.3.1)."""
+ return self.downloadOrder.call(data, self._gen_func_id())
+
+ def call_confirmOrder(self, data: dict) -> dict:
+ """Perform ES2+ ConfirmOrder function (SGP.22 section 5.3.2)."""
+ return self.confirmOrder.call(data, self._gen_func_id())
+
+ def call_cancelOrder(self, data: dict) -> dict:
+ """Perform ES2+ CancelOrder function (SGP.22 section 5.3.3)."""
+ return self.cancelOrder.call(data, self._gen_func_id())
+
+ def call_releaseProfile(self, data: dict) -> dict:
+ """Perform ES2+ CancelOrder function (SGP.22 section 5.3.4)."""
+ return self.releaseProfile.call(data, self._gen_func_id())
+
+ def call_handleDownloadProgressInfo(self, data: dict) -> dict:
+ """Perform ES2+ HandleDownloadProgressInfo function (SGP.22 section 5.3.5)."""
+ return self.handleDownloadProgressInfo.call(data, self._gen_func_id())
diff --git a/pySim/esim/es8p.py b/pySim/esim/es8p.py
new file mode 100644
index 0000000..81b0fc9
--- /dev/null
+++ b/pySim/esim/es8p.py
@@ -0,0 +1,185 @@
+# Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+
+# as per SGP22 v3.0 Section 5.5
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from typing import Dict, List, Optional
+from pySim.utils import b2h, h2b, bertlv_encode_tag, bertlv_encode_len
+
+import pySim.esim.rsp as rsp
+from pySim.esim.bsp import BspInstance
+
+# Given that GSMA RSP uses ASN.1 in a very weird way, we actually cannot encode the full data type before
+# signing, but we have to build parts of it separately first, then sign that, so we can put the signature
+# into the same sequence as the signed data. We use the existing pySim TLV code for this.
+
+def wrap_as_der_tlv(tag: int, val: bytes) -> bytes:
+ """Wrap the 'value' into a DER-encoded TLV."""
+ return bertlv_encode_tag(tag) + bertlv_encode_len(len(val)) + val
+
+def gen_init_sec_chan_signed_part(iscsp: Dict) -> bytes:
+ """Generate the concatenated remoteOpId, transactionId, controlRefTemplate and smdpOtpk data objects
+ without the outer SEQUENCE tag / length or the remainder of initialiseSecureChannel, as is required
+ for signing purpose."""
+ out = b''
+ out += wrap_as_der_tlv(0x82, bytes([iscsp['remoteOpId']]))
+ out += wrap_as_der_tlv(0x80, iscsp['transactionId'])
+
+ crt = iscsp['controlRefTemplate']
+ out_crt = wrap_as_der_tlv(0x80, crt['keyType'])
+ out_crt += wrap_as_der_tlv(0x81, crt['keyLen'])
+ out_crt += wrap_as_der_tlv(0x84, crt['hostId'])
+ out += wrap_as_der_tlv(0xA6, out_crt)
+
+ out += wrap_as_der_tlv(0x5F49, iscsp['smdpOtpk'])
+ return out
+
+
+# SGP.22 Section 5.5.1
+def gen_initialiseSecureChannel(transactionId: str, host_id: bytes, smdp_otpk: bytes, euicc_otpk: bytes, dp_pb):
+ """Generate decoded representation of (signed) initialiseSecureChannel (SGP.22 5.5.2)"""
+ init_scr = { 'remoteOpId': 1, # installBoundProfilePackage
+ 'transactionId': h2b(transactionId),
+ # GlobalPlatform Card Specification Amendment F [13] section 6.5.2.3 for the Mutual Authentication Data Field
+ 'controlRefTemplate': { 'keyType': bytes([0x88]), 'keyLen': bytes([16]), 'hostId': host_id },
+ 'smdpOtpk': smdp_otpk, # otPK.DP.KA
+ }
+ to_sign = gen_init_sec_chan_signed_part(init_scr) + wrap_as_der_tlv(0x5f49, euicc_otpk)
+ init_scr['smdpSign'] = dp_pb.ecdsa_sign(to_sign)
+ return init_scr
+
+def gen_replace_session_keys(ppk_enc: bytes, ppk_cmac: bytes, initial_mcv: bytes) -> bytes:
+ """Generate encoded (but unsigned) ReplaceSessionKeysReqest DO (SGP.22 5.5.4)"""
+ rsk = { 'ppkEnc': ppk_enc, 'ppkCmac': ppk_cmac, 'initialMacChainingValue': initial_mcv }
+ return rsp.asn1.encode('ReplaceSessionKeysRequest', rsk)
+
+
+class ProfileMetadata:
+ """Representation of Profile metadata. Right now only the mandatory bits are
+ supported, but in general this should follow the StoreMetadataRequest of SGP.22 5.5.3"""
+ def __init__(self, iccid_bin: bytes, spn: str, profile_name: str):
+ self.iccid_bin = iccid_bin
+ self.spn = spn
+ self.profile_name = profile_name
+
+ def gen_store_metadata_request(self) -> bytes:
+ """Generate encoded (but unsigned) StoreMetadataReqest DO (SGP.22 5.5.3)"""
+ smr = {
+ 'iccid': self.iccid_bin,
+ 'serviceProviderName': self.spn,
+ 'profileName': self.profile_name,
+ }
+ return rsp.asn1.encode('StoreMetadataRequest', smr)
+
+
+class ProfilePackage:
+ def __init__(self, metadata: Optional[ProfileMetadata] = None):
+ self.metadata = metadata
+
+class UnprotectedProfilePackage(ProfilePackage):
+ """Representing an unprotected profile package (UPP) as defined in SGP.22 Section 2.5.2"""
+
+ @classmethod
+ def from_der(cls, der: bytes, metadata: Optional[ProfileMetadata] = None) -> 'UnprotectedProfilePackage':
+ """Load an UPP from its DER representation."""
+ inst = cls(metadata=metadata)
+ cls.der = der
+ # TODO: we later certainly want to parse it so we can perform modification (IMSI, key material, ...)
+ # just like in the traditional SIM/USIM dynamic data phase at the end of personalization
+ return inst
+
+ def to_der(self):
+ """Return the DER representation of the UPP."""
+ # TODO: once we work on decoded structures, we may want to re-encode here
+ return self.der
+
+class ProtectedProfilePackage(ProfilePackage):
+ """Representing a protected profile package (PPP) as defined in SGP.22 Section 2.5.3"""
+
+ @classmethod
+ def from_upp(cls, upp: UnprotectedProfilePackage, bsp: BspInstance) -> 'ProtectedProfilePackage':
+ """Generate the PPP as a sequence of encrypted and MACed Command TLVs representing the UPP"""
+ inst = cls(metadata=upp.metadata)
+ inst.upp = upp
+ # store ppk-enc, ppc-mac
+ inst.ppk_enc = bsp.c_algo.s_enc
+ inst.ppk_mac = bsp.m_algo.s_mac
+ inst.initial_mcv = bsp.m_algo.mac_chain
+ inst.encoded = bsp.encrypt_and_mac(0x86, upp.to_der())
+ return inst
+
+ #def __val__(self):
+ #return self.encoded
+
+class BoundProfilePackage(ProfilePackage):
+ """Representing a bound profile package (BPP) as defined in SGP.22 Section 2.5.4"""
+
+ @classmethod
+ def from_ppp(cls, ppp: ProtectedProfilePackage):
+ inst = cls()
+ inst.upp = None
+ inst.ppp = ppp
+ return inst
+
+ @classmethod
+ def from_upp(cls, upp: UnprotectedProfilePackage):
+ inst = cls()
+ inst.upp = upp
+ inst.ppp = None
+ return inst
+
+ def encode(self, ss: 'RspSessionState', dp_pb: 'CertAndPrivkey') -> bytes:
+ """Generate a bound profile package (SGP.22 2.5.4)."""
+
+ def encode_seq(tag: int, sequence: List[bytes]) -> bytes:
+ """Encode a "sequenceOfXX" as specified in SGP.22 specifying the raw SEQUENCE OF tag,
+ and assuming the caller provides the fully-encoded (with TAG + LEN) member TLVs."""
+ payload = b''.join(sequence)
+ return bertlv_encode_tag(tag) + bertlv_encode_len(len(payload)) + payload
+
+ bsp = BspInstance.from_kdf(ss.shared_secret, 0x88, 16, ss.host_id, h2b(ss.eid))
+
+ iscr = gen_initialiseSecureChannel(ss.transactionId, ss.host_id, ss.smdp_otpk, ss.euicc_otpk, dp_pb)
+ # generate unprotected input data
+ conf_idsp_bin = rsp.asn1.encode('ConfigureISDPRequest', {})
+ if self.upp:
+ smr_bin = self.upp.metadata.gen_store_metadata_request()
+ else:
+ smr_bin = self.ppp.metadata.gen_store_metadata_request()
+
+ # we don't use rsp.asn1.encode('boundProfilePackage') here, as the BSP already provides
+ # fully encoded + MACed TLVs including their tag + length values. We cannot put those as
+ # 'value' input into an ASN.1 encoder, as that would double the TAG + LENGTH :(
+
+ # 'initialiseSecureChannelRequest'
+ bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
+ # firstSequenceOf87
+ bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
+ # sequenceOF88
+ bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
+
+ if self.ppp: # we have to use session keys
+ rsk_bin = gen_replace_session_keys(self.ppp.ppk_enc, self.ppp.ppk_mac, self.ppp.initial_mcv)
+ # secondSequenceOf87
+ bpp_seq += encode_seq(0xa2, bsp.encrypt_and_mac(0x87, rsk_bin))
+ else:
+ self.ppp = ProtectedProfilePackage.from_upp(self.upp, bsp)
+
+ # 'sequenceOf86'
+ bpp_seq += encode_seq(0xa3, self.ppp.encoded)
+
+ # manual DER encode: wrap in outer SEQUENCE
+ return bertlv_encode_tag(0xbf36) + bertlv_encode_len(len(bpp_seq)) + bpp_seq
diff --git a/pySim/esim/rsp.py b/pySim/esim/rsp.py
new file mode 100644
index 0000000..c2a163b
--- /dev/null
+++ b/pySim/esim/rsp.py
@@ -0,0 +1,131 @@
+# Implementation of GSMA eSIM RSP (Remote SIM Provisioning)
+# as per SGP22 v3.0
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from typing import Optional
+import shelve
+
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.serialization import Encoding
+from cryptography import x509
+
+from pySim.utils import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv, b2h
+from pySim.esim import compile_asn1_subdir
+
+asn1 = compile_asn1_subdir('rsp')
+
+class RspSessionState:
+ """Encapsulates the state of a RSP session. It is created during the initiateAuthentication
+ and subsequently used by further API calls using the same transactionId. The session state
+ is removed either after cancelSession or after notification.
+ TODO: add some kind of time based expiration / garbage collection."""
+ def __init__(self, transactionId: str, serverChallenge: bytes, ci_cert_id: bytes):
+ self.transactionId = transactionId
+ self.serverChallenge = serverChallenge
+ # used at a later point between API calls
+ self.ci_cert_id = ci_cert_id
+ self.euicc_cert: Optional[x509.Certificate] = None
+ self.eum_cert: Optional[x509.Certificate] = None
+ self.eid: Optional[bytes] = None
+ self.profileMetadata: Optional['ProfileMetadata'] = None
+ self.smdpSignature2_do = None
+ # really only needed while processing getBoundProfilePackage request?
+ self.euicc_otpk: Optional[bytes] = None
+ self.smdp_ot: Optional[ec.EllipticCurvePrivateKey] = None
+ self.smdp_otpk: Optional[bytes] = None
+ self.host_id: Optional[bytes] = None
+ self.shared_secret: Optional[bytes] = None
+
+
+ def __getstate__(self):
+ """helper function called when pickling the object to persistent storage. We must pickel all
+ members that are not pickle-able."""
+ state = self.__dict__.copy()
+ # serialize eUICC certificate as DER
+ if state.get('euicc_cert', None):
+ state['_euicc_cert'] = self.euicc_cert.public_bytes(Encoding.DER)
+ del state['euicc_cert']
+ # serialize EUM certificate as DER
+ if state.get('eum_cert', None):
+ state['_eum_cert'] = self.eum_cert.public_bytes(Encoding.DER)
+ del state['eum_cert']
+ # serialize one-time SMDP private key to integer + curve
+ if state.get('smdp_ot', None):
+ state['_smdp_otsk'] = self.smdp_ot.private_numbers().private_value
+ state['_smdp_ot_curve'] = self.smdp_ot.curve
+ del state['smdp_ot']
+ return state
+
+ def __setstate__(self, state):
+ """helper function called when unpickling the object from persistent storage. We must recreate all
+ members from the state generated in __getstate__ above."""
+ # restore eUICC certificate from DER
+ if '_euicc_cert' in state:
+ self.euicc_cert = x509.load_der_x509_certificate(state['_euicc_cert'])
+ del state['_euicc_cert']
+ else:
+ self.euicc_cert = None
+ # restore EUM certificate from DER
+ if '_eum_cert' in state:
+ self.eum_cert = x509.load_der_x509_certificate(state['_eum_cert'])
+ del state['_eum_cert']
+ # restore one-time SMDP private key from integer + curve
+ if state.get('_smdp_otsk', None):
+ self.smdp_ot = ec.derive_private_key(state['_smdp_otsk'], state['_smdp_ot_curve'])
+ # FIXME: how to add the public key from smdp_otpk to an instance of EllipticCurvePrivateKey?
+ del state['_smdp_otsk']
+ del state['_smdp_ot_curve']
+ # automatically recover all the remainig state
+ self.__dict__.update(state)
+
+
+class RspSessionStore(shelve.DbfilenameShelf):
+ """A derived class as wrapper around the database-backed non-volatile storage 'shelve', in case we might
+ need to extend it in the future. We use it to store RspSessionState objects indexed by transactionId."""
+
+def extract_euiccSigned1(authenticateServerResponse: bytes) -> bytes:
+ """Extract the raw, DER-encoded binary euiccSigned1 field from the given AuthenticateServerResponse. This
+ is needed due to the very peculiar SGP.22 notion of signing sections of DER-encoded ASN.1 objects."""
+ rawtag, l, v, remainder = bertlv_parse_one_rawtag(authenticateServerResponse)
+ if len(remainder):
+ raise ValueError('Excess data at end of TLV')
+ if rawtag != 0xbf38:
+ raise ValueError('Unexpected outer tag: %s' % b2h(rawtag))
+ rawtag, l, v1, remainder = bertlv_parse_one_rawtag(v)
+ if rawtag != 0xa0:
+ raise ValueError('Unexpected tag where CHOICE was expected')
+ rawtag, l, tlv2, remainder = bertlv_return_one_rawtlv(v1)
+ if rawtag != 0x30:
+ raise ValueError('Unexpected tag where SEQUENCE was expected')
+ return tlv2
+
+def extract_euiccSigned2(prepareDownloadResponse: bytes) -> bytes:
+ """Extract the raw, DER-encoded binary euiccSigned2 field from the given prepareDownloadrResponse. This is
+ needed due to the very peculiar SGP.22 notion of signing sections of DER-encoded ASN.1 objects."""
+ rawtag, l, v, remainder = bertlv_parse_one_rawtag(prepareDownloadResponse)
+ if len(remainder):
+ raise ValueError('Excess data at end of TLV')
+ if rawtag != 0xbf21:
+ raise ValueError('Unexpected outer tag: %s' % b2h(rawtag))
+ rawtag, l, v1, remainder = bertlv_parse_one_rawtag(v)
+ if rawtag != 0xa0:
+ raise ValueError('Unexpected tag where CHOICE was expected')
+ rawtag, l, tlv2, remainder = bertlv_return_one_rawtlv(v1)
+ if rawtag != 0x30:
+ raise ValueError('Unexpected tag where SEQUENCE was expected')
+ return tlv2
diff --git a/pySim/esim/saip/__init__.py b/pySim/esim/saip/__init__.py
new file mode 100644
index 0000000..e5b99e7
--- /dev/null
+++ b/pySim/esim/saip/__init__.py
@@ -0,0 +1,513 @@
+# Implementation of SimAlliance/TCA Interoperable Profile handling
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import abc
+import io
+from typing import Tuple, List, Optional, Dict, Union
+from collections import OrderedDict
+
+import asn1tools
+
+from pySim.utils import bertlv_parse_tag, bertlv_parse_len, b2h, h2b
+from pySim.ts_102_221 import FileDescriptor
+from pySim.construct import build_construct
+from pySim.esim import compile_asn1_subdir
+from pySim.esim.saip import templates
+from pySim.tlv import BER_TLV_IE
+from pySim.global_platform import KeyType, KeyUsageQualifier
+from pySim.global_platform.uicc import UiccSdInstallParams
+
+asn1 = compile_asn1_subdir('saip')
+
+class File:
+ """Internal representation of a file in a profile filesystem.
+
+ Parameters:
+ pename: Name string of the profile element
+ l: List of tuples [fileDescriptor, fillFileContent, fillFileOffset profile elements]
+ template: Applicable FileTemplate describing defaults as per SAIP spec
+ """
+ def __init__(self, pename: str, l: Optional[List[Tuple]] = None, template: Optional[templates.FileTemplate] = None):
+ self.pe_name = pename
+ self.template = template
+ self.fileDescriptor = {}
+ self.stream = None
+ # apply some defaults from profile
+ if self.template:
+ self.from_template(self.template)
+ print("after template: %s" % repr(self))
+ if l:
+ self.from_tuples(l)
+
+ def from_template(self, template: templates.FileTemplate):
+ """Determine defaults for file based on given FileTemplate."""
+ fdb_dec = {}
+ self.rec_len = None
+ if template.fid:
+ self.fileDescriptor['fileID'] = template.fid.to_bytes(2, 'big')
+ if template.sfi:
+ self.fileDescriptor['shortEFID'] = bytes([template.sfi])
+ if template.arr:
+ self.fileDescriptor['securityAttributesReferenced'] = bytes([template.arr])
+ # All the files defined in the templates shall have, by default, shareable/not-shareable bit in the file descriptor set to "shareable".
+ fdb_dec['shareable'] = True
+ if template.file_type in ['LF', 'CY']:
+ fdb_dec['file_type'] = 'working_ef'
+ if template.rec_len:
+ self.record_len = template.rec_len
+ if template.nb_rec and template.rec_len:
+ self.fileDescriptor['efFileSize'] = (template.nb_rec * template.rec_len).to_bytes(2, 'big') # FIXME
+ if template.file_type == 'LF':
+ fdb_dec['structure'] = 'linear_fixed'
+ elif template.file_type == 'CY':
+ fdb_dec['structure'] = 'cyclic'
+ elif template.file_type in ['TR', 'BT']:
+ fdb_dec['file_type'] = 'working_ef'
+ if template.file_size:
+ self.fileDescriptor['efFileSize'] = template.file_size.to_bytes(2, 'big') # FIXME
+ if template.file_type == 'BT':
+ fdb_dec['structure'] = 'ber_tlv'
+ elif template.file_type == 'TR':
+ fdb_dec['structure'] = 'transparent'
+ elif template.file_type in ['MF', 'DF', 'ADF']:
+ fdb_dec['file_type'] = 'df'
+ fdb_dec['structure'] = 'no_info_given'
+ # build file descriptor based on above input data
+ fd_dict = {'file_descriptor_byte': fdb_dec}
+ if self.rec_len:
+ fd_dict['record_len'] = self.rec_len
+ self.fileDescriptor['fileDescriptor'] = build_construct(FileDescriptor._construct, fd_dict)
+ # FIXME: default_val
+ # FIXME: high_update
+ # FIXME: params?
+
+ def from_tuples(self, l:List[Tuple]):
+ """Parse a list of fileDescriptor, fillFileContent, fillFileOffset tuples into this instance."""
+ def get_fileDescriptor(l:List[Tuple]):
+ for k, v in l:
+ if k == 'fileDescriptor':
+ return v
+ fd = get_fileDescriptor(l)
+ if not fd:
+ raise ValueError("No fileDescriptor found")
+ self.fileDescriptor.update(dict(fd))
+ self.stream = self.linearize_file_content(l)
+
+ def to_tuples(self) -> List[Tuple]:
+ """Generate a list of fileDescriptor, fillFileContent, fillFileOffset tuples into this instance."""
+ raise NotImplementedError
+
+ @staticmethod
+ def linearize_file_content(l: List[Tuple]) -> Optional[io.BytesIO]:
+ """linearize a list of fillFileContent / fillFileOffset tuples into a stream of bytes."""
+ stream = io.BytesIO()
+ for k, v in l:
+ if k == 'doNotCreate':
+ return None
+ if k == 'fileDescriptor':
+ pass
+ elif k == 'fillFileOffset':
+ stream.write(b'\xff' * v)
+ elif k == 'fillFileContent':
+ stream.write(v)
+ else:
+ return ValueError("Unknown key '%s' in tuple list" % k)
+ return stream
+
+ def __str__(self) -> str:
+ return "File(%s)" % self.pe_name
+
+ def __repr__(self) -> str:
+ return "File(%s): %s" % (self.pe_name, self.fileDescriptor)
+
+class ProfileElement:
+ """Class representing a Profile Element (PE) within a SAIP Profile."""
+ FILE_BEARING = ['mf', 'cd', 'telecom', 'usim', 'opt-usim', 'isim', 'opt-isim', 'phonebook', 'gsm-access',
+ 'csim', 'opt-csim', 'eap', 'df-5gs', 'df-saip', 'df-snpn', 'df-5gprose', 'iot', 'opt-iot']
+ # in their infinite wisdom the spec authors used inconsistent/irregular naming of PE type vs. hedaer field
+ # names, so we have to manually translate the exceptions here...
+ header_name_translation_dict = {
+ 'header': None,
+ 'genericFileManagement': 'gfm-header',
+ 'akaParameter': 'aka-header',
+ 'cdmaParameter': 'cdma-header',
+ # note how they couldn't even consistently captialize the 'header' suffix :(
+ 'application': 'app-Header',
+ 'pukCodes': 'puk-Header',
+ 'pinCodes': 'pin-Header',
+ 'securityDomain': 'sd-Header',
+ }
+
+ def __init__(self, decoded = None):
+ self.decoded = decoded
+
+ def _fixup_sqnInit_dec(self) -> None:
+ """asn1tools has a bug when working with SEQUENCE OF that have DEFAULT values. Let's work around
+ this."""
+ if self.type != 'akaParameter':
+ return
+ sqn_init = self.decoded.get('sqnInit', None)
+ if not sqn_init:
+ return
+ # this weird '0x' value in a string is what we get from our (slightly hacked) ASN.1 syntax
+ if sqn_init == '0x000000000000':
+ # SEQUENCE (SIZE (32)) OF OCTET STRING (SIZE (6))
+ self.decoded['sqnInit'] = [b'\x00'*6] * 32
+
+ def _fixup_sqnInit_enc(self) -> None:
+ """asn1tools has a bug when working with SEQUENCE OF that have DEFAULT values. Let's work around
+ this."""
+ if self.type != 'akaParameter':
+ return
+ sqn_init = self.decoded.get('sqnInit', None)
+ if not sqn_init:
+ return
+ for s in sqn_init:
+ if any(s):
+ return
+ # none of the fields were initialized with a non-default (non-zero) value, so we can skip it
+ del self.decoded['sqnInit']
+
+ @property
+ def header_name(self) -> str:
+ """Return the name of the header field within the profile element."""
+ # unneccessarry compliaction by inconsistent naming :(
+ if self.type.startswith('opt-'):
+ return self.type.replace('-','') + '-header'
+ if self.type in self.header_name_translation_dict:
+ return self.header_name_translation_dict[self.type]
+ return self.type + '-header'
+
+ @property
+ def header(self):
+ """Return the decoded ProfileHeader."""
+ return self.decoded.get(self.header_name, None)
+
+ @property
+ def templateID(self):
+ """Return the decoded templateID used by this profile element (if any)."""
+ return self.decoded.get('templateID', None)
+
+ @property
+ def files(self):
+ """Return dict of decoded 'File' ASN.1 items."""
+ if not self.type in self.FILE_BEARING:
+ return {}
+ return {k:v for (k,v) in self.decoded.items() if k not in ['templateID', self.header_name]}
+
+ @classmethod
+ def from_der(cls, der: bytes) -> 'ProfileElement':
+ """Construct an instance from given raw, DER encoded bytes."""
+ pe_type, decoded = asn1.decode('ProfileElement', der)
+ if pe_type == 'securityDomain':
+ inst = ProfileElementSD(decoded)
+ else:
+ inst = ProfileElement(decoded)
+ inst.type = pe_type
+ # work around asn1tools bug regarding DEFAULT for a SEQUENCE OF
+ inst._fixup_sqnInit_dec()
+ # run any post-decoder a derived class may have
+ if hasattr(inst, '_post_decode'):
+ inst._post_decode()
+ return inst
+
+ def to_der(self) -> bytes:
+ """Build an encoded DER representation of the instance."""
+ # run any pre-encoder a derived class may have
+ if hasattr(self, '_pre_encode'):
+ self._pre_encode()
+ # work around asn1tools bug regarding DEFAULT for a SEQUENCE OF
+ self._fixup_sqnInit_enc()
+ return asn1.encode('ProfileElement', (self.type, self.decoded))
+
+ def __str__(self) -> str:
+ return self.type
+
+class SecurityDomainKeyComponent:
+ """Representation of a key-component of a key for a security domain."""
+ def __init__(self, key_type: str, key_data: bytes, mac_length: int = 8):
+ self.key_type = key_type
+ self.key_data = key_data
+ self.mac_length = mac_length
+
+ def __repr__(self) -> str:
+ return 'SdKeyComp(type=%s, mac_len=%u, data=%s)' % (self.key_type, self.mac_length,
+ b2h(self.key_data))
+
+
+ @classmethod
+ def from_saip_dict(cls, saip: dict) -> 'SecurityDomainKeyComponent':
+ """Construct instance from the dict as generated by SAIP asn.1 decoder."""
+ return cls(KeyType.parse(saip['keyType']), saip['keyData'], saip['macLength'])
+
+ def to_saip_dict(self) -> dict:
+ """Express instance in the dict format required by SAIP asn.1 encoder."""
+ return {'keyType': KeyType.build(self.key_type),
+ 'keyData': self.key_data,
+ 'macLength': self.mac_length}
+
+class SecurityDomainKey:
+ """Represenation of a key used for SCP access to a security domain."""
+ def __init__(self, key_version_number: int, key_id: int, key_usage_qualifier: dict,
+ key_components: List[SecurityDomainKeyComponent]):
+ self.key_usage_qualifier = key_usage_qualifier
+ self.key_identifier = key_id
+ self.key_version_number = key_version_number
+ self.key_components = key_components
+
+ def __repr__(self) -> str:
+ return 'SdKey(KVN=0x%02x, ID=0x%02x, Usage=%s, Comp=%s)' % (self.key_version_number,
+ self.key_identifier,
+ self.key_usage_qualifier,
+ repr(self.key_components))
+
+ @classmethod
+ def from_saip_dict(cls, saip: dict) -> 'SecurityDomainKey':
+ """Construct instance from the dict as generated by SAIP asn.1 decoder."""
+ inst = cls(int.from_bytes(saip['keyVersionNumber'], "big"),
+ int.from_bytes(saip['keyIdentifier'], "big"),
+ KeyUsageQualifier.parse(saip['keyUsageQualifier']),
+ [SecurityDomainKeyComponent.from_saip_dict(x) for x in saip['keyComponents']])
+ return inst
+
+ def to_saip_dict(self) -> dict:
+ """Express instance in the dict format required by SAIP asn.1 encoder."""
+ return {'keyUsageQualifier': KeyUsageQualifier.build(self.key_usage_qualifier),
+ 'keyIdentifier': bytes([self.key_identifier]),
+ 'keyVersionNumber': bytes([self.key_version_number]),
+ 'keyComponents': [k.to_saip_dict() for k in self.key_components]}
+
+class ProfileElementSD(ProfileElement):
+ """Class representing a securityDomain ProfileElement."""
+ type = 'securityDomain'
+
+ class C9(BER_TLV_IE, tag=0xC9, nested=UiccSdInstallParams):
+ pass
+
+ def __init__(self, decoded: Optional[dict] = None):
+ if decoded:
+ self.decoded = decoded
+ return
+ # provide some reasonable defaults for a MNO-SD
+ self.decoded = OrderedDict()
+ self.decoded['sd-Header'] = { 'mandated': None, 'identification': None }
+ self.decoded['instance'] = {
+ 'applicationLoadPackageAID': h2b('A0000001515350'),
+ 'classAID': h2b('A000000251535041'),
+ 'instanceAID': h2b('A000000151000000'),
+ # Optional: extraditeSecurityDomainAID
+ 'applicationPrivileges': h2b('82FC80'),
+ # Optioal: lifeCycleState
+ 'applicationSpecificParametersC9': h2b('8201f09301f08701f0'), # we assume user uses add_scp()
+ # Optional: systemSpecificParameters
+ 'applicationParameters': {
+ # TAR: B20100, MSL: 12
+ 'uiccToolkitApplicationSpecificParametersField': h2b('0100000100000002011203B2010000'),
+ },
+ # Optional: processData
+ # Optional: controlReferenceTemplate
+ }
+ self.decoded['keyList'] = [] # we assume user uses add_key() method for all keys
+ # Optional: sdPersoData
+ # Optional: openPersoData
+ # Optional: catTpParameters
+ self._post_decode()
+
+ def _post_decode(self):
+ self.usip = self.C9()
+ self.usip.from_bytes(self.decoded['instance']['applicationSpecificParametersC9'])
+ self.keys = [SecurityDomainKey.from_saip_dict(x) for x in self.decoded['keyList']]
+
+ def _pre_encode(self):
+ self.decoded['keyList'] = [x.to_saip_dict() for x in self.keys]
+ self.decoded['instance']['applicationSpecificParametersC9'] = self.usip.to_bytes()
+
+ def has_scp(self, scp: int) -> bool:
+ """Determine if SD Installation parameters already specify given SCP."""
+ return self.usip.nested_collection.has_scp(scp)
+
+ def add_scp(self, scp: int, i: int):
+ """Add given SCP (and i parameter) to list of SCP of the Security Domain Install Params.
+ Example: add_scp(0x03, 0x70) for SCP03, or add_scp(0x02, 0x55) for SCP02."""
+ self.usip.nested_collection.add_scp(scp, i)
+ self._pre_encode()
+
+ def remove_scp(self, scp: int):
+ """Remove given SCP from list of SCP of the Security Domain Install Params."""
+ self.usip.nested_collection.remove_scp(scp)
+ self._pre_encode()
+
+ def find_key(self, key_version_number: int, key_id: int) -> Optional[SecurityDomainKey]:
+ """Find and return (if any) the SecurityDomainKey for given KVN + KID."""
+ for k in self.keys:
+ if k.key_version_number == key_version_number and k.key_identifier == key_id:
+ return k
+ return None
+
+ def add_key(self, key: SecurityDomainKey):
+ """Add a given SecurityDomainKey to the keyList of the securityDomain."""
+ if self.find_key(key.key_version_number, key.key_identifier):
+ raise ValueError('Key for KVN=0x%02x / KID=0x%02x already exists' % (key.key_version_number,
+ key.key_identifier))
+ self.keys.append(key)
+ self._pre_encode()
+
+ def remove_key(self, key_version_number: int, key_id: int):
+ key = self.find_key(key_version_number, key_id)
+ if not key:
+ raise ValueError('No key for KVN=0x%02x / KID=0x%02x found' % (key_version_number, key_id))
+ self.keys.remove(key)
+ self._pre_encode()
+
+class ProfileElementSSD(ProfileElementSD):
+ """Class representing a securityDomain ProfileElement for a SSD."""
+ def __init__(self):
+ super().__init__()
+ # defaults [overriding ProfileElementSD) taken from SAIP v2.3.1 Section 11.2.12
+ self.decoded['instance']['instanceAID'] = h2b('A00000055910100102736456616C7565')
+ self.decoded['instance']['applicationPrivileges'] = h2b('808000')
+ self.decoded['instance']['applicationParameters'] = {
+ # TAR: 6C7565, MSL: 12
+ 'uiccToolkitApplicationSpecificParametersField': h2b('01000001000000020112036C756500'),
+ }
+
+def bertlv_first_segment(binary: bytes) -> Tuple[bytes, bytes]:
+ """obtain the first segment of a binary concatenation of BER-TLV objects.
+ Returns: tuple of first TLV and remainder."""
+ _tagdict, remainder = bertlv_parse_tag(binary)
+ length, remainder = bertlv_parse_len(remainder)
+ tl_length = len(binary) - len(remainder)
+ tlv_length = tl_length + length
+ return binary[:tlv_length], binary[tlv_length:]
+
+class ProfileElementSequence:
+ """A sequence of ProfileElement objects, which is the overall representation of an eSIM profile."""
+ def __init__(self):
+ self.pe_list: List[ProfileElement] = None
+ self.pe_by_type: Dict = {}
+ self.pes_by_naa: Dict = {}
+
+ def get_pes_for_type(self, tname: str) -> List[ProfileElement]:
+ """Return list of profile elements present for given profile element type."""
+ return self.pe_by_type.get(tname, [])
+
+ def get_pe_for_type(self, tname: str) -> Optional[ProfileElement]:
+ """Return a single profile element for given profile element type. Works only for
+ types of which there is only a signle instance in the PE Sequence!"""
+ l = self.get_pes_for_type(tname)
+ if len(l) == 0:
+ return None
+ assert len(l) == 1
+ return l[0]
+
+ def parse_der(self, der: bytes) -> None:
+ """Parse a sequence of PE and store the result in self.pe_list."""
+ self.pe_list = []
+ remainder = der
+ while len(remainder):
+ first_tlv, remainder = bertlv_first_segment(remainder)
+ self.pe_list.append(ProfileElement.from_der(first_tlv))
+ self._process_pelist()
+
+ def _process_pelist(self) -> None:
+ self._rebuild_pe_by_type()
+ self._rebuild_pes_by_naa()
+
+ def _rebuild_pe_by_type(self) -> None:
+ self.pe_by_type = {}
+ # build a dict {pe_type: [pe, pe, pe]}
+ for pe in self.pe_list:
+ if pe.type in self.pe_by_type:
+ self.pe_by_type[pe.type].append(pe)
+ else:
+ self.pe_by_type[pe.type] = [pe]
+
+ def _rebuild_pes_by_naa(self) -> None:
+ """rebuild the self.pes_by_naa dict {naa: [ [pe, pe, pe], [pe, pe] ]} form,
+ which basically means for every NAA there's a lsit of instances, and each consists
+ of a list of a list of PEs."""
+ self.pres_by_naa = {}
+ petype_not_naa_related = ['securityDomain', 'rfm', 'application', 'end']
+ naa = ['mf', 'usim', 'isim', 'csim']
+ cur_naa = None
+ cur_naa_list = []
+ for pe in self.pe_list:
+ # skip all PE that are not related to NAA
+ if pe.type in petype_not_naa_related:
+ continue
+ if pe.type in naa:
+ if cur_naa:
+ if not cur_naa in self.pes_by_naa:
+ self.pes_by_naa[cur_naa] = []
+ self.pes_by_naa[cur_naa].append(cur_naa_list)
+ cur_naa = pe.type
+ cur_naa_list = []
+ cur_naa_list.append(pe)
+ # append the final one
+ if cur_naa and len(cur_naa_list) > 0:
+ if not cur_naa in self.pes_by_naa:
+ self.pes_by_naa[cur_naa] = []
+ self.pes_by_naa[cur_naa].append(cur_naa_list)
+
+ @classmethod
+ def from_der(cls, der: bytes) -> 'ProfileElementSequence':
+ """Construct an instance from given raw, DER encoded bytes."""
+ inst = cls()
+ inst.parse_der(der)
+ return inst
+
+ def to_der(self) -> bytes:
+ """Build an encoded DER representation of the instance."""
+ out = b''
+ for pe in self.pe_list:
+ out += pe.to_der()
+ return out
+
+ def renumber_identification(self):
+ """Re-generate the 'identification' numbering of all PE headers."""
+ i = 1
+ for pe in self.pe_list:
+ hdr = pe.header
+ if not hdr:
+ continue
+ pe.header['identification'] = i
+ i += 1
+
+ def get_index_by_type(self, petype: str) -> List[int]:
+ """Return a list with the indicies of all instances of PEs of petype."""
+ ret = []
+ i = 0
+ for pe in self.pe_list:
+ if pe.type == petype:
+ ret.append(i)
+ i += 1
+ return ret
+
+ def add_ssd(self, ssd: ProfileElementSSD):
+ """Add a SSD (Supplementary Security Domain) After MNO-SD/ISD-P."""
+ # find MNO-SD index
+ idx = self.get_index_by_type('securityDomain')[0]
+ # insert _after_ MNO-SD
+ self.pe_list.insert(idx+1, ssd)
+ self._process_pelist()
+ self.renumber_identification()
+
+ def __repr__(self) -> str:
+ return "PESequence(%s)" % ', '.join([str(x) for x in self.pe_list])
+
+ def __iter__(self) -> str:
+ yield from self.pe_list
diff --git a/pySim/esim/saip/oid.py b/pySim/esim/saip/oid.py
new file mode 100644
index 0000000..238e49f
--- /dev/null
+++ b/pySim/esim/saip/oid.py
@@ -0,0 +1,77 @@
+# Implementation of SimAlliance/TCA Interoperable Profile OIDs
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from typing import List, Union
+
+class OID:
+ @staticmethod
+ def intlist_from_str(instr: str) -> List[int]:
+ return [int(x) for x in instr.split('.')]
+
+ @staticmethod
+ def str_from_intlist(intlist: List[int]) -> str:
+ return '.'.join([str(x) for x in intlist])
+
+ def __init__(self, initializer: Union[List[int], str]):
+ if isinstance(initializer, str):
+ self.intlist = self.intlist_from_str(initializer)
+ else:
+ self.intlist = initializer
+
+ def __str__(self) -> str:
+ return self.str_from_intlist(self.intlist)
+
+ def __repr__(self) -> str:
+ return 'OID(%s)' % (str(self))
+
+
+class eOID(OID):
+ """OID helper for TCA eUICC prefix"""
+ __prefix = [2,23,143,1]
+ def __init__(self, initializer):
+ if isinstance(initializer, str):
+ initializer = self.intlist_from_str(initializer)
+ super().__init__(self.__prefix + initializer)
+
+MF = eOID("2.1")
+DF_CD = eOID("2.2")
+DF_TELECOM = eOID("2.3")
+DF_TELECOM_v2 = eOID("2.3.2")
+ADF_USIM_by_default = eOID("2.4")
+ADF_USIM_by_default_v2 = eOID("2.4.2")
+ADF_USIM_not_by_default = eOID("2.5")
+ADF_USIM_not_by_default_v2 = eOID("2.5.2")
+ADF_USIM_not_by_default_v3 = eOID("2.5.3")
+DF_PHONEBOOK_ADF_USIM = eOID("2.6")
+DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
+ADF_ISIM_by_default = eOID("2.8")
+ADF_ISIM_not_by_default = eOID("2.9")
+ADF_ISIM_not_by_default_v2 = eOID("2.9.2")
+ADF_CSIM_by_default = eOID("2.10")
+ADF_CSIM_by_default_v2 = eOID("2.10.2")
+ADF_CSIM_not_by_default = eOID("2.11")
+ADF_CSIM_not_by_default_v2 = eOID("2.11.2")
+DF_EAP = eOID("2.12")
+DF_5GS = eOID("2.13")
+DF_5GS_v2 = eOID("2.13.2")
+DF_5GS_v3 = eOID("2.13.3")
+DF_5GS_v4 = eOID("2.13.4")
+DF_SAIP = eOID("2.14")
+DF_SNPN = eOID("2.15")
+DF_5GProSe = eOID("2.16")
+IoT_default = eOID("2.17")
+IoT_default = eOID("2.18")
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
new file mode 100644
index 0000000..dc3435b
--- /dev/null
+++ b/pySim/esim/saip/personalization.py
@@ -0,0 +1,324 @@
+# Implementation of SimAlliance/TCA Interoperable Profile handling
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import abc
+import io
+from typing import List, Tuple
+
+from pySim.tlv import camel_to_snake
+from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
+from pySim.esim.saip import ProfileElement, ProfileElementSequence
+
+def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) -> List[Tuple]:
+ """In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
+ return list(filter(lambda x: x[0] not in unwanted_keys, l))
+
+def file_replace_content(file: List[Tuple], new_content: bytes):
+ """Completely replace all fillFileContent of a decoded 'File' with the new_content."""
+ # use [:] to avoid making a copy, as we're doing in-place modification of the list here
+ file[:] = remove_unwanted_tuples_from_list(file, ['fillFileContent', 'fillFileOffset'])
+ file.append(('fillFileContent', new_content))
+ return file
+
+class ClassVarMeta(abc.ABCMeta):
+ """Metaclass that puts all additional keyword-args into the class. We use this to have one
+ class definition for something like a PIN, and then have derived classes for PIN1, PIN2, ..."""
+ def __new__(metacls, name, bases, namespace, **kwargs):
+ #print("Meta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
+ x = super().__new__(metacls, name, bases, namespace)
+ for k, v in kwargs.items():
+ setattr(x, k, v)
+ setattr(x, 'name', camel_to_snake(name))
+ return x
+
+class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
+ """Base class representing a part of the eSIM profile that is configurable during the
+ personalization process (with dynamic data from elsewhere)."""
+ def __init__(self, input_value):
+ self.input_value = input_value # the raw input value as given by caller
+ self.value = None # the processed input value (e.g. with check digit) as produced by validate()
+
+ def validate(self):
+ """Optional validation method. Can be used by derived classes to perform validation
+ of the input value (self.value). Will raise an exception if validation fails."""
+ # default implementation: simply copy input_value over to value
+ self.value = self.input_value
+
+ @abc.abstractmethod
+ def apply(self, pes: ProfileElementSequence):
+ pass
+
+class Iccid(ConfigurableParameter):
+ """Configurable ICCID. Expects the value to be a string of decimal digits.
+ If the string of digits is only 18 digits long, a Luhn check digit will be added."""
+
+ def validate(self):
+ # convert to string as it migt be an integer
+ iccid_str = str(self.input_value)
+ if len(iccid_str) < 18 or len(iccid_str) > 20:
+ raise ValueError('ICCID must be 18, 19 or 20 digits long')
+ if not iccid_str.isdecimal():
+ raise ValueError('ICCID must only contain decimal digits')
+ self.value = sanitize_iccid(iccid_str)
+
+ def apply(self, pes: ProfileElementSequence):
+ # patch the header
+ pes.get_pe_for_type('header').decoded['iccid'] = h2b(rpad(self.value, 20))
+ # patch MF/EF.ICCID
+ file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(self.value)))
+
+class Imsi(ConfigurableParameter):
+ """Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
+ the last digit of the IMSI."""
+
+ def validate(self):
+ # convert to string as it migt be an integer
+ imsi_str = str(self.input_value)
+ if len(imsi_str) < 6 or len(imsi_str) > 15:
+ raise ValueError('IMSI must be 6..15 digits long')
+ if not imsi_str.isdecimal():
+ raise ValueError('IMSI must only contain decimal digits')
+ self.value = imsi_str
+
+ def apply(self, pes: ProfileElementSequence):
+ imsi_str = self.value
+ # we always use the least significant byte of the IMSI as ACC
+ acc = (1 << int(imsi_str[-1]))
+ # patch ADF.USIM/EF.IMSI
+ for pe in pes.get_pes_for_type('usim'):
+ file_replace_content(pe.decoded['ef-imsi'], h2b(enc_imsi(imsi_str)))
+ file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
+ # TODO: DF.GSM_ACCESS if not linked?
+
+
+class SdKey(ConfigurableParameter, metaclass=ClassVarMeta):
+ """Configurable Security Domain (SD) Key. Value is presented as bytes."""
+ # these will be set by derived classes
+ key_type = None
+ key_id = None
+ kvn = None
+ key_usage_qual = None
+ permitted_len = None
+
+ def validate(self):
+ if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
+ raise ValueError('Value must be of bytes-like type')
+ if self.permitted_len:
+ if len(self.input_value) not in self.permitted_len:
+ raise ValueError('Value length must be %s' % self.permitted_len)
+ self.value = self.input_value
+
+ def _apply_sd(self, pe: ProfileElement):
+ assert pe.type == 'securityDomain'
+ for key in pe.decoded['keyList']:
+ if key['keyIdentifier'][0] == self.key_id and key['keyVersionNumber'][0] == self.kvn:
+ assert len(key['keyComponents']) == 1
+ key['keyComponents'][0]['keyData'] = self.value
+ return
+ # Could not find matching key to patch, create a new one
+ key = {
+ 'keyUsageQualifier': bytes([self.key_usage_qual]),
+ 'keyIdentifier': bytes([self.key_id]),
+ 'keyVersionNumber': bytes([self.kvn]),
+ 'keyComponents': [
+ { 'keyType': bytes([self.key_type]), 'keyData': self.value },
+ ]
+ }
+ pe.decoded['keyList'].append(key)
+
+ def apply(self, pes: ProfileElementSequence):
+ for pe in pes.get_pes_for_type('securityDomain'):
+ self._apply_sd(pe)
+
+class SdKeyScp80_01(SdKey, kvn=0x01, key_type=0x88, permitted_len=[16,24,32]): # AES key type
+ pass
+class SdKeyScp80_01Kic(SdKeyScp80_01, key_id=0x01, key_usage_qual=0x18): # FIXME: ordering?
+ pass
+class SdKeyScp80_01Kid(SdKeyScp80_01, key_id=0x02, key_usage_qual=0x14):
+ pass
+class SdKeyScp80_01Kik(SdKeyScp80_01, key_id=0x03, key_usage_qual=0x48):
+ pass
+
+class SdKeyScp81_01(SdKey, kvn=0x81): # FIXME
+ pass
+class SdKeyScp81_01Psk(SdKeyScp81_01, key_id=0x01, key_type=0x85, key_usage_qual=0x3C):
+ pass
+class SdKeyScp81_01Dek(SdKeyScp81_01, key_id=0x02, key_type=0x88, key_usage_qual=0x48):
+ pass
+
+class SdKeyScp02_20(SdKey, kvn=0x20, key_type=0x88, permitted_len=[16,24,32]): # AES key type
+ pass
+class SdKeyScp02_20Enc(SdKeyScp02_20, key_id=0x01, key_usage_qual=0x18):
+ pass
+class SdKeyScp02_20Mac(SdKeyScp02_20, key_id=0x02, key_usage_qual=0x14):
+ pass
+class SdKeyScp02_20Dek(SdKeyScp02_20, key_id=0x03, key_usage_qual=0x48):
+ pass
+
+class SdKeyScp03_30(SdKey, kvn=0x30, key_type=0x88, permitted_len=[16,24,32]): # AES key type
+ pass
+class SdKeyScp03_30Enc(SdKeyScp03_30, key_id=0x01, key_usage_qual=0x18):
+ pass
+class SdKeyScp03_30Mac(SdKeyScp03_30, key_id=0x02, key_usage_qual=0x14):
+ pass
+class SdKeyScp03_30Dek(SdKeyScp03_30, key_id=0x03, key_usage_qual=0x48):
+ pass
+
+class SdKeyScp03_31(SdKey, kvn=0x31, key_type=0x88, permitted_len=[16,24,32]): # AES key type
+ pass
+class SdKeyScp03_31Enc(SdKeyScp03_31, key_id=0x01, key_usage_qual=0x18):
+ pass
+class SdKeyScp03_31Mac(SdKeyScp03_31, key_id=0x02, key_usage_qual=0x14):
+ pass
+class SdKeyScp03_31Dek(SdKeyScp03_31, key_id=0x03, key_usage_qual=0x48):
+ pass
+
+class SdKeyScp03_32(SdKey, kvn=0x32, key_type=0x88, permitted_len=[16,24,32]): # AES key type
+ pass
+class SdKeyScp03_32Enc(SdKeyScp03_32, key_id=0x01, key_usage_qual=0x18):
+ pass
+class SdKeyScp03_32Mac(SdKeyScp03_32, key_id=0x02, key_usage_qual=0x14):
+ pass
+class SdKeyScp03_32Dek(SdKeyScp03_32, key_id=0x03, key_usage_qual=0x48):
+ pass
+
+
+
+
+def obtain_singleton_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
+ filtered = list(filter(lambda x: x.type == wanted_type, l))
+ assert len(filtered) == 1
+ return filtered[0]
+
+def obtain_first_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
+ filtered = list(filter(lambda x: x.type == wanted_type, l))
+ return filtered[0]
+
+class Puk(ConfigurableParameter, metaclass=ClassVarMeta):
+ """Configurable PUK (Pin Unblock Code). String ASCII-encoded digits."""
+ keyReference = None
+ def validate(self):
+ if isinstance(self.input_value, int):
+ self.value = '%08d' % self.input_value
+ else:
+ self.value = self.input_value
+ # FIXME: valid length?
+ if not self.value.isdecimal():
+ raise ValueError('PUK must only contain decimal digits')
+
+ def apply(self, pes: ProfileElementSequence):
+ puk = ''.join(['%02x' % (ord(x)) for x in self.value])
+ padded_puk = rpad(puk, 16)
+ mf_pes = pes.pes_by_naa['mf'][0]
+ pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
+ for pukCode in pukCodes.decoded['pukCodes']:
+ if pukCode['keyReference'] == self.keyReference:
+ pukCode['pukValue'] = h2b(padded_puk)
+ return
+ raise ValueError('cannot find pukCode')
+class Puk1(Puk, keyReference=0x01):
+ pass
+class Puk2(Puk, keyReference=0x81):
+ pass
+
+class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
+ """Configurable PIN (Personal Identification Number). String of digits."""
+ keyReference = None
+ def validate(self):
+ if isinstance(self.input_value, int):
+ self.value = '%04d' % self.input_value
+ else:
+ self.value = self.input_value
+ if len(self.value) < 4 or len(self.value) > 8:
+ raise ValueError('PIN mus be 4..8 digits long')
+ if not self.value.isdecimal():
+ raise ValueError('PIN must only contain decimal digits')
+ def apply(self, pes: ProfileElementSequence):
+ pin = ''.join(['%02x' % (ord(x)) for x in self.value])
+ padded_pin = rpad(pin, 16)
+ mf_pes = pes.pes_by_naa['mf'][0]
+ pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
+ if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
+ return
+ for pinCode in pinCodes.decoded['pinCodes'][1]:
+ if pinCode['keyReference'] == self.keyReference:
+ pinCode['pinValue'] = h2b(padded_pin)
+ return
+ raise ValueError('cannot find pinCode')
+class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
+ """Configurable PIN (Personal Identification Number). String of digits."""
+ keyReference = None
+ def validate(self):
+ if isinstance(self.input_value, int):
+ self.value = '%04d' % self.input_value
+ else:
+ self.value = self.input_value
+ if len(self.value) < 4 or len(self.value) > 8:
+ raise ValueError('PIN mus be 4..8 digits long')
+ if not self.value.isdecimal():
+ raise ValueError('PIN must only contain decimal digits')
+ def _apply_one(self, pe: ProfileElement):
+ pin = ''.join(['%02x' % (ord(x)) for x in self.value])
+ padded_pin = rpad(pin, 16)
+ pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes')
+ if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
+ return
+ for pinCode in pinCodes.decoded['pinCodes'][1]:
+ if pinCode['keyReference'] == self.keyReference:
+ pinCode['pinValue'] = h2b(padded_pin)
+ return
+ raise ValueError('cannot find pinCode')
+ def apply(self, pes: ProfileElementSequence):
+ for naa in pes.pes_by_naa:
+ if naa not in ['usim','isim','csim','telecom']:
+ continue
+ for instance in pes.pes_by_naa[naa]:
+ self._apply_one(instance)
+class Pin1(Pin, keyReference=0x01):
+ pass
+# PIN2 is special: telecom + usim + isim + csim
+class Pin2(AppPin, keyReference=0x81):
+ pass
+class Adm1(Pin, keyReference=0x0A):
+ pass
+class Adm2(Pin, keyReference=0x0B):
+ pass
+
+
+class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
+ """Configurable Algorithm parameter. bytes."""
+ key = None
+ def validate(self):
+ if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
+ raise ValueError('Value must be of bytes-like type')
+ self.value = self.input_value
+ def apply(self, pes: ProfileElementSequence):
+ for pe in pes.get_pes_for_type('akaParameter'):
+ algoConfiguration = pe.decoded['algoConfiguration']
+ if algoConfiguration[0] != 'algoParameter':
+ continue
+ algoConfiguration[1][self.key] = self.value
+
+class K(AlgoConfig, key='key'):
+ pass
+class Opc(AlgoConfig, key='opc'):
+ pass
+class AlgorithmID(AlgoConfig, key='algorithmID'):
+ def validate(self):
+ if self.input_value not in [1, 2, 3]:
+ raise ValueError('Invalid algorithmID %s' % (self.input_value))
+ self.value = self.input_value
diff --git a/pySim/esim/saip/templates.py b/pySim/esim/saip/templates.py
new file mode 100644
index 0000000..cd76247
--- /dev/null
+++ b/pySim/esim/saip/templates.py
@@ -0,0 +1,675 @@
+# Implementation of SimAlliance/TCA Interoperable Profile Template handling
+#
+# (C) 2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from typing import *
+from copy import deepcopy
+import pySim.esim.saip.oid as OID
+
+class FileTemplate:
+ """Representation of a single file in a SimAlliance/TCA Profile Template."""
+ def __init__(self, fid:int, name:str, ftype, nb_rec: Optional[int], size:Optional[int], arr:int,
+ sfi:Optional[int] = None, default_val:Optional[str] = None, content_rqd:bool = True,
+ params:Optional[List] = None, ass_serv:Optional[List[int]]=None, high_update:bool = False,
+ pe_name:Optional[str] = None):
+ # initialize from arguments
+ self.fid = fid
+ self.name = name
+ if pe_name:
+ self.pe_name = pe_name
+ else:
+ self.pe_name = self.name.replace('.','-').replace('_','-').lower()
+ self.file_type = ftype
+ if ftype in ['LF', 'CY']:
+ self.nb_rec = nb_rec
+ self.rec_len = size
+ elif ftype in ['TR']:
+ self.file_size = size
+ self.arr = arr
+ self.sfi = sfi
+ self.default_val = default_val
+ self.content_rqd = content_rqd
+ self.params = params
+ self.ass_serv = ass_serv
+ self.high_update = high_update
+ # initialize empty
+ self.parent = None
+ self.children = []
+
+ def __str__(self) -> str:
+ return "FileTemplate(%s)" % (self.name)
+
+ def __repr__(self) -> str:
+ s_fid = "%04x" % self.fid if self.fid is not None else 'None'
+ s_arr = self.arr if self.arr is not None else 'None'
+ s_sfi = "%02x" % self.sfi if self.sfi is not None else 'None'
+ return "FileTemplate(%s/%s, %s, %s, arr=%s, sfi=%s)" % (self.name, self.pe_name, s_fid,
+ self.file_type, s_arr, s_sfi)
+
+class ProfileTemplate:
+ """Representation of a SimAlliance/TCA Profile Template. Each Template is identified by its OID and
+ consists of a number of file definitions. We implement each profile template as a class derived from this
+ base class. Each such derived class is a singleton and has no instances."""
+ created_by_default: bool = False
+ oid: Optional[OID.eOID] = None
+ files: List[FileTemplate] = []
+ files_by_pename: dict[str,FileTemplate] = {}
+
+ def __init_subclass__(cls, **kwargs):
+ """This classmethod is called automatically after executing the subclass body. We use it to
+ initialize the cls.files_by_pename from the cls.files"""
+ super().__init_subclass__(**kwargs)
+ for f in cls.files:
+ cls.files_by_pename[f.pe_name] = f
+ ProfileTemplateRegistry.add(cls)
+
+class ProfileTemplateRegistry:
+ """A registry of profile templates. Exists as a singleton class with no instances and only
+ classmethods."""
+ by_oid = {}
+
+ @classmethod
+ def add(cls, tpl: ProfileTemplate):
+ """Add a ProfileTemplate to the registry. There can only be one Template per OID."""
+ oid_str = str(tpl.oid)
+ if oid_str in cls.by_oid:
+ raise ValueError("We already have a template for OID %s" % oid_str)
+ cls.by_oid[oid_str] = tpl
+
+ @classmethod
+ def get_by_oid(cls, oid: Union[List[int], str]) -> Optional[ProfileTemplate]:
+ """Look-up the ProfileTemplate based on its OID. The OID can be given either in dotted-string format,
+ or as a list of integers."""
+ if not isinstance(oid, str):
+ oid = OID.OID.str_from_intlist(oid)
+ return cls.by_oid.get(oid, None)
+
+# below are transcribed template definitions from "ANNEX A (Normative): File Structure Templates Definition"
+# of "Profile interoperability specification V3.1 Final" (unless other version explicitly specified).
+
+# Section 9.2
+class FilesAtMF(ProfileTemplate):
+ created_by_default = True
+ oid = OID.MF
+ files = [
+ FileTemplate(0x3f00, 'MF', 'MF', None, None, 14, None, None, None, params=['pinStatusTemplateDO']),
+ FileTemplate(0x2f05, 'EF.PL', 'TR', None, 2, 1, 0x05, 'FF...FF', None),
+ FileTemplate(0x2f02, 'EF.ICCID', 'TR', None, 10, 11, None, None, True),
+ FileTemplate(0x2f00, 'EF.DIR', 'LF', None, None, 10, 0x1e, None, True, params=['nb_rec', 'size']),
+ FileTemplate(0x2f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, params=['nb_rec', 'size']),
+ FileTemplate(0x2f08, 'EF.UMPC', 'TR', None, 5, 10, 0x08, None, False),
+ ]
+
+
+# Section 9.3
+class FilesCD(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_CD
+ files = [
+ FileTemplate(0x7f11, 'DF.CD', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
+ FileTemplate(0x6f01, 'EF.LAUNCHPAD', 'TR', None, None, 2, None, None, True, params=['size']),
+ ]
+ for i in range(0x40, 0x7f):
+ files.append(FileTemplate(0x6f00+i, 'EF.ICON', 'TR', None, None, 2, None, None, True, params=['size']))
+
+
+# Section 9.4: Do this separately, so we can use them also from 9.5.3
+df_pb_files = [
+ FileTemplate(0x5f3a, 'DF.PHONEBOOK', 'DF', None, None, 14, None, None, True, ['pinStatusTemplateDO']),
+ FileTemplate(0x4f30, 'EF.PBR', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size']),
+]
+for i in range(0x38, 0x40):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi']))
+for i in range(0x40, 0x48):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
+for i in range(0x48, 0x50):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
+df_pb_files += [
+ FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi']),
+ FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
+ FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
+]
+for i in range(0x50, 0x58):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
+for i in range(0x58, 0x60):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
+for i in range(0x60, 0x68):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi']))
+for i in range(0x68, 0x70):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
+for i in range(0x70, 0x78):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi']))
+for i in range(0x78, 0x80):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
+for i in range(0x80, 0x88):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
+for i in range(0x88, 0x90):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi']))
+for i in range(0x90, 0x98):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi']))
+for i in range(0x98, 0xa0):
+ df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
+
+# Section 9.4 v2.3.1
+class FilesTelecom(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_TELECOM
+ files = [
+ FileTemplate(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
+ FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
+ FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
+ FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
+ FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
+ FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
+ FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
+ FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
+ FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
+ # EF.IIDF below
+ FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size']),
+ FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
+ # EF.ICON below
+ ]
+ for i in range(0x40, 0x80):
+ files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
+ for i in range(0x80, 0xC0):
+ files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
+
+ # we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
+ files += [deepcopy(x) for x in df_pb_files]
+
+ files += [
+ FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
+ FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
+ FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
+
+ FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
+ FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
+ FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
+ FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
+ ]
+
+
+# Section 9.4
+class FilesTelecomV2(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_TELECOM_v2
+ files = [
+ FileTemplate(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
+ FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
+ FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
+ FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
+ FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
+ FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
+ FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
+ FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
+ FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
+ # EF.IIDF below
+ FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size']),
+ FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
+ # EF.ICON below
+ ]
+ for i in range(0x40, 0x80):
+ files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
+ for i in range(0x80, 0xC0):
+ files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
+
+ # we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
+ files += [deepcopy(x) for x in df_pb_files]
+
+ files += [
+ FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
+ FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
+ FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
+
+ FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
+ FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
+ FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
+ FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
+
+
+ FileTemplate(0x5f3d, 'DF.MCS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv={'usim':109, 'isim': 15}),
+ FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
+ FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
+
+ FileTemplate(0x5f3e, 'DF.V2X', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[119]),
+ FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119]),
+ FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119]),
+ FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 2
+ FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 3
+ ]
+
+
+# Section 9.5.1 v2.3.1
+class FilesUsimMandatory(ProfileTemplate):
+ created_by_default = True
+ oid = OID.ADF_USIM_by_default
+ files = [
+ FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
+ FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
+ FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
+ FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
+ FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name = 'ef-keysPS'),
+ FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
+ FileTemplate(0x6f38, 'EF.UST', 'TR', None, 14, 2, 0x04, None, True),
+ FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
+ FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
+ FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
+ FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
+ FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
+ FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
+ FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
+ FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
+ FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
+ FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
+ FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
+ FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
+ FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
+ FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
+ FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
+ FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
+ FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
+ ]
+
+# Section 9.5.1
+class FilesUsimMandatoryV2(ProfileTemplate):
+ created_by_default = True
+ oid = OID.ADF_USIM_by_default_v2
+ files = [
+ FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
+ FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
+ FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
+ FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
+ FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name='ef-keysPS'),
+ FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
+ FileTemplate(0x6f38, 'EF.UST', 'TR', None, 17, 2, 0x04, None, True),
+ FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
+ FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
+ FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
+ FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
+ FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
+ FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
+ FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
+ FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
+ FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
+ FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
+ FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
+ FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
+ FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
+ FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
+ FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
+ FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
+ FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
+ ]
+
+
+# Section 9.5.2 v2.3.1
+class FilesUsimOptional(ProfileTemplate):
+ created_by_default = False
+ oid = OID.ADF_USIM_not_by_default
+ files = [
+ FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
+ FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13], pe_name='ef-acmax'),
+ FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
+ FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
+ FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
+ FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
+ FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
+ FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
+ FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
+ FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
+ FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
+ FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
+ FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
+ FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000'*8, False, ass_serv=[20]),
+ FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000'*8, False, ass_serv=[42]),
+ FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43]),
+ FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
+ FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
+ FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
+ FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
+ FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
+ FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
+ FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
+ FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
+ FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
+ FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
+ FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
+ FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
+ FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
+ FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
+ FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
+ FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
+ FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
+ FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
+ FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
+ FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
+ FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
+ FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
+ FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
+ FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
+ FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
+ FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
+ FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
+ FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
+ FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
+ FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
+ FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
+ FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
+ FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
+ FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
+ FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
+ FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
+ FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
+ FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
+ FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
+ FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
+ FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
+ FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
+ FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
+ FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
+ FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
+ FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
+ FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
+ FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
+ FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
+ FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
+ FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
+ FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
+ FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
+ FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
+ FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
+ FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
+ FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
+ FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
+ ]
+
+
+# Section 9.5.2
+class FilesUsimOptionalV2(ProfileTemplate):
+ created_by_default = False
+ oid = OID.ADF_USIM_not_by_default_v2
+ files = [
+ FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
+ FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13]),
+ FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
+ FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
+ FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
+ FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
+ FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
+ FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
+ FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
+ FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
+ FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
+ FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
+ FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
+ FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000'*8, False, ass_serv=[20]),
+ FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000'*8, False, ass_serv=[42]),
+ FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43]),
+ FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
+ FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
+ FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
+ FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
+ FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
+ FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
+ FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
+ FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
+ FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
+ FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
+ FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
+ FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
+ FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
+ FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
+ FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
+ FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
+ FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
+ FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
+ FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
+ FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
+ FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
+ FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
+ FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
+ FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
+ FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
+ FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
+ FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
+ FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
+ FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
+ FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
+ FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
+ FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
+ FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
+ FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
+ FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
+ FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
+ FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
+ FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
+ FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
+ FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
+ FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
+ FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
+ FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
+ FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
+ FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
+ FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
+ FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
+ FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
+ FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
+ FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
+ FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
+ FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
+ FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
+ FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
+ FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
+ FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
+ FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
+ FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
+ FileTemplate(0x6ff3, 'EF.EPDGID', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
+ FileTemplate(0x6ff4, 'EF.EPDGSELECTION','TR',None,None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
+ FileTemplate(0x6ff5, 'EF.EPDGIDEM', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
+ FileTemplate(0x6ff6, 'EF.EPDGIDEMSEL','TR',None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
+ FileTemplate(0x6ff7, 'EF.FromPreferred','TR',None, 1, 2, None, '00', False, ass_serv=[114]),
+ FileTemplate(0x6ff8, 'EF.IMSConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[115]),
+ FileTemplate(0x6ff9, 'EF.3GPPPSDataOff','TR',None, 4, 2, None, None, True, ass_serv=[117]),
+ FileTemplate(0x6ffa, 'EF.3GPPPSDOSLIST','LF',None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[118]),
+ FileTemplate(0x6ffc, 'EF.XCAPConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[120]),
+ FileTemplate(0x6ffd, 'EF.EARFCNLIST','TR', None, None, 10, None, None, True, ['size'], ass_serv=[121]),
+ FileTemplate(0x6ffd, 'EF.MudMidCfgdata','BT', None, None,2, None, None, True, ['size'], ass_serv=[134]),
+ ]
+
+
+# Section 9.5.3
+class FilesUsimDfPhonebook(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_PHONEBOOK_ADF_USIM
+ files = df_pb_files
+
+
+# Section 9.5.4
+class FilesUsimDfGsmAccess(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_GSM_ACCESS_ADF_USIM
+ files = [
+ FileTemplate(0x5f3b, 'DF.GSM-ACCESS','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[27]),
+ FileTemplate(0x4f20, 'EF.Kc', 'TR', None, 9, 5, 0x01, 'FF...FF07', False, ass_serv=[27], high_update=True),
+ FileTemplate(0x4f52, 'EF.KcGPRS', 'TR', None, 9, 5, 0x02, 'FF...FF07', False, ass_serv=[27], high_update=True),
+ FileTemplate(0x4f63, 'EF.CPBCCH', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[39], high_update=True),
+ FileTemplate(0x4f64, 'EF.InvScan', 'TR', None, 1, 2, None, '00', False, ass_serv=[40]),
+ ]
+
+
+# Section 9.5.11 v2.3.1
+class FilesUsimDf5GS(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_5GS
+ files = [
+ FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
+ FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
+ FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
+ FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
+ FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
+ FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
+ FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
+ ]
+
+
+# Section 9.5.11.2
+class FilesUsimDf5GSv2(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_5GS_v2
+ files = [
+ FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
+ FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
+ FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
+ FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
+ FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
+ FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
+ FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
+ FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
+ FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
+ ]
+
+
+# Section 9.5.11.3
+class FilesUsimDf5GSv3(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_5GS_v3
+ files = [
+ FileTemplate(0x6fc0, 'DF.5GS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[122,126,127,128,129,130], pe_name='df-df-5gs'),
+ FileTemplate(0x4f01, 'EF.5GS3GPPLOCI', 'TR', None, 20, 5, 0x01, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
+ FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 2, 62, 5, 0x03, 'FF...FF', False, ass_serv=[122,136], high_update=True),
+ # ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
+ FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 2, 62, 5, 0x04, 'FF...FF', False, ass_serv=[122,136], high_update=True),
+ # ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
+ FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
+ FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
+ FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
+ FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
+ FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130], pe_name='ef-supinai'),
+ FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
+ FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
+ FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
+ ]
+
+
+# Section 9.5.12
+class FilesUsimDfSaip(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_SAIP
+ files = [
+ FileTemplate(0x6fd0, 'DF.SAIP', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[(124, 125)], pe_name='df-df-saip'),
+ FileTemplate(0x4f01, 'EF.SUCICalcInfo','TR', None, None, 3, None, 'FF..FF', False, ['size'], ass_serv=[125], pe_name='ef-suci-calc-info-usim'),
+ ]
+
+
+# Section 9.6.1
+class FilesIsimMandatory(ProfileTemplate):
+ created_by_default = True
+ oid = OID.ADF_ISIM_by_default
+ files = [
+ FileTemplate( None, 'ADF.ISIM', 'ADF', None, None, 14, None, None, False, ['aid','temporary_fid','pinStatusTemplateDO']),
+ FileTemplate(0x6f02, 'EF.IMPI', 'TR', None, None, 2, 0x02, None, True, ['size']),
+ FileTemplate(0x6f04, 'EF.IMPU', 'LF', 1, None, 2, 0x04, None, True, ['size']),
+ FileTemplate(0x6f03, 'EF.Domain', 'TR', None, None, 2, 0x05, None, True, ['size']),
+ FileTemplate(0x6f07, 'EF.IST', 'TR', None, 14, 2, 0x07, None, True),
+ FileTemplate(0x6fad, 'EF.AD', 'TR', None, 3, 10, 0x03, '000000', False),
+ FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x06, None, True, ['nb_rec','size']),
+ ]
+
+
+# Section 9.6.2 v2.3.1
+class FilesIsimOptional(ProfileTemplate):
+ created_by_default = False
+ oid = OID.ADF_ISIM_not_by_default
+ files = [
+ FileTemplate(0x6f09, 'EF.P-CSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
+ FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
+ FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
+ FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
+ FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
+ FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
+ FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
+ FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
+ FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
+ ]
+
+
+# Section 9.6.2
+class FilesIsimOptionalv2(ProfileTemplate):
+ created_by_default = False
+ oid = OID.ADF_ISIM_not_by_default_v2
+ files = [
+ FileTemplate(0x6f09, 'EF.PCSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
+ FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
+ FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
+ FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
+ FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
+ FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
+ FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
+ FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
+ FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
+ FileTemplate(0x6ff7, 'EF.FromPreferred','TR', None, 1, 2, None, '00', False, ass_serv=[17]),
+ FileTemplate(0x6ff8, 'EF.ImsConfigData','BT', None,None, 2, None, None, True, ['size'], ass_serv=[18]),
+ FileTemplate(0x6ffc, 'EF.XcapconfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[19]),
+ FileTemplate(0x6ffa, 'EF.WebRTCURI', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size'], ass_serv=[20]),
+ FileTemplate(0x6ffa, 'EF.MudMidCfgData','BT',None, None, 2, None, None, True, ['size'], ass_serv=[21]),
+ ]
+
+
+# TODO: CSIM
+
+
+# Section 9.8
+class FilesEap(ProfileTemplate):
+ created_by_default = False
+ oid = OID.DF_EAP
+ files = [
+ FileTemplate( None, 'DF.EAP', 'DF', None, None, 14, None, None, False, ['fid','pinStatusTemplateDO'], ass_serv=[(124, 125)]),
+ FileTemplate(0x4f01, 'EF.EAPKEYS', 'TR', None, None, 2, None, None, True, ['size'], high_update=True),
+ FileTemplate(0x4f02, 'EF.EAPSTATUS', 'TR', None, 1, 2, None, '00', False, high_update=True),
+ FileTemplate(0x4f03, 'EF.PUId', 'TR', None, None, 2, None, None, True, ['size']),
+ FileTemplate(0x4f04, 'EF.Ps', 'TR', None, None, 5, None, 'FF..FF', False, ['size'], high_update=True),
+ FileTemplate(0x4f20, 'EF.CurID', 'TR', None, None, 5, None, 'FF..FF', False, ['size'], high_update=True),
+ FileTemplate(0x4f21, 'EF.RelID', 'TR', None, None, 5, None, None, True, ['size']),
+ FileTemplate(0x4f22, 'EF.Realm', 'TR', None, None, 5, None, None, True, ['size']),
+ ]
+
+
+# Section 9.9 Access Rules Definition
+ARR_DEFINITION = {
+ 1: ['8001019000', '800102A406830101950108', '800158A40683010A950108'],
+ 2: ['800101A406830101950108', '80015AA40683010A950108'],
+ 3: ['80015BA40683010A950108'],
+ 4: ['8001019000', '80011A9700', '800140A40683010A950108'],
+ 5: ['800103A406830101950108', '800158A40683010A950108'],
+ 6: ['800111A406830101950108', '80014AA40683010A950108'],
+ 7: ['800103A406830101950108', '800158A40683010A950108', '840132A406830101950108'],
+ 8: ['800101A406830101950108', '800102A406830181950108', '800158A40683010A950108'],
+ 9: ['8001019000', '80011AA406830101950108', '800140A40683010A950108'],
+ 10: ['8001019000', '80015AA40683010A950108'],
+ 11: ['8001019000', '800118A40683010A950108', '8001429700'],
+ 12: ['800101A406830101950108', '80015A9700'],
+ 13: ['800113A406830101950108', '800148A40683010A950108'],
+ 14: ['80015EA40683010A950108'],
+}
diff --git a/pySim/esim/saip/validation.py b/pySim/esim/saip/validation.py
new file mode 100644
index 0000000..c253b7a
--- /dev/null
+++ b/pySim/esim/saip/validation.py
@@ -0,0 +1,134 @@
+# Implementation of SimAlliance/TCA Interoperable Profile handling
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from pySim.esim.saip import *
+
+class ProfileError(Exception):
+ pass
+
+class ProfileConstraintChecker:
+ def check(self, pes: ProfileElementSequence):
+ for name in dir(self):
+ if name.startswith('check_'):
+ method = getattr(self, name)
+ method(pes)
+
+class CheckBasicStructure(ProfileConstraintChecker):
+ def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
+ opt_pe = pes.get_pe_for_type(opt)
+ if opt_pe:
+ after_pe = pes.get_pe_for_type(after)
+ if not after_pe:
+ raise ProfileError('PE-%s without PE-%s' % (opt.upper(), after.upper()))
+ # FIXME: check order
+
+ def check_start_and_end(self, pes: ProfileElementSequence):
+ if pes.pe_list[0].type != 'header':
+ raise ProfileError('first element is not header')
+ if pes.pe_list[1].type != 'mf':
+ # strictly speaking: permitted, but we don't support MF via GenericFileManagement
+ raise ProfileError('second element is not mf')
+ if pes.pe_list[-1].type != 'end':
+ raise ProfileError('last element is not end')
+
+ def check_number_of_occurrence(self, pes: ProfileElementSequence):
+ # check for invalid number of occurrences
+ if len(pes.get_pes_for_type('header')) != 1:
+ raise ProfileError('multiple ProfileHeader')
+ if len(pes.get_pes_for_type('mf')) != 1:
+ # strictly speaking: 0 permitted, but we don't support MF via GenericFileManagement
+ raise ProfileError('multiple PE-MF')
+ for tn in ['end', 'cd', 'telecom',
+ 'usim', 'isim', 'csim', 'opt-usim','opt-isim','opt-csim',
+ 'df-saip', 'df-5gs']:
+ if len(pes.get_pes_for_type(tn)) > 1:
+ raise ProfileError('multiple PE-%s' % tn.upper())
+
+ def check_optional_ordering(self, pes: ProfileElementSequence):
+ # ordering and required depenencies
+ self._is_after_if_exists(pes,'opt-usim', 'usim')
+ self._is_after_if_exists(pes,'opt-isim', 'isim')
+ self._is_after_if_exists(pes,'gsm-access', 'usim')
+ self._is_after_if_exists(pes,'phonebook', 'usim')
+ self._is_after_if_exists(pes,'df-5gs', 'usim')
+ self._is_after_if_exists(pes,'df-saip', 'usim')
+ self._is_after_if_exists(pes,'opt-csim', 'csim')
+
+ def check_mandatory_services(self, pes: ProfileElementSequence):
+ """Ensure that the PE for the mandatory services exist."""
+ m_svcs = pes.get_pe_for_type('header').decoded['eUICC-Mandatory-services']
+ if 'usim' in m_svcs and not pes.get_pe_for_type('usim'):
+ raise ProfileError('no PE-USIM for mandatory usim service')
+ if 'isim' in m_svcs and not pes.get_pe_for_type('isim'):
+ raise ProfileError('no PE-ISIM for mandatory isim service')
+ if 'csim' in m_svcs and not pes.get_pe_for_type('csim'):
+ raise ProfileError('no PE-ISIM for mandatory csim service')
+ if 'gba-usim' in m_svcs and not 'usim' in m_svcs:
+ raise ProfileError('gba-usim mandatory, but no usim')
+ if 'gba-isim' in m_svcs and not 'isim' in m_svcs:
+ raise ProfileError('gba-isim mandatory, but no isim')
+ if 'multiple-usim' in m_svcs and not 'usim' in m_svcs:
+ raise ProfileError('multiple-usim mandatory, but no usim')
+ if 'multiple-isim' in m_svcs and not 'isim' in m_svcs:
+ raise ProfileError('multiple-isim mandatory, but no isim')
+ if 'multiple-csim' in m_svcs and not 'csim' in m_svcs:
+ raise ProfileError('multiple-csim mandatory, but no csim')
+ if 'get-identity' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
+ raise ProfileError('get-identity mandatory, but no usim or isim')
+ if 'profile-a-x25519' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
+ raise ProfileError('profile-a-x25519 mandatory, but no usim or isim')
+ if 'profile-a-p256' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
+ raise ProfileError('profile-a-p256 mandatory, but no usim or isim')
+
+ def check_identification_unique(self, pes: ProfileElementSequence):
+ """Ensure that each PE has a unique identification value."""
+ id_list = [pe.header['identification'] for pe in pes.pe_list if pe.header]
+ if len(id_list) != len(set(id_list)):
+ raise ProfileError('PE identification values are not unique')
+
+FileChoiceList = List[Tuple]
+
+class FileError(ProfileError):
+ pass
+
+class FileConstraintChecker:
+ def check(self, l: FileChoiceList):
+ for name in dir(self):
+ if name.startswith('check_'):
+ method = getattr(self, name)
+ method(l)
+
+class FileCheckBasicStructure(FileConstraintChecker):
+ def check_seqence(self, l: FileChoiceList):
+ by_type = {}
+ for k, v in l:
+ if k in by_type:
+ by_type[k].append(v)
+ else:
+ by_type[k] = [v]
+ if 'doNotCreate' in by_type:
+ if len(l) != 1:
+ raise FileError("doNotCreate must be the only element")
+ if 'fileDescriptor' in by_type:
+ if len(by_type['fileDescriptor']) != 1:
+ raise FileError("fileDescriptor must be the only element")
+ if l[0][0] != 'fileDescriptor':
+ raise FileError("fileDescriptor must be the first element")
+
+ def check_forbidden(self, l: FileChoiceList):
+ """Perform checks for forbidden parameters as described in Section 8.3.3."""
diff --git a/pySim/esim/x509_cert.py b/pySim/esim/x509_cert.py
new file mode 100644
index 0000000..3bcf8a2
--- /dev/null
+++ b/pySim/esim/x509_cert.py
@@ -0,0 +1,210 @@
+# Implementation of X.509 certificate handling in GSMA eSIM
+# as per SGP22 v3.0
+#
+# (C) 2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import requests
+from typing import Optional, List
+
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives import hashes
+from cryptography import x509
+from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding
+from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
+
+from pySim.utils import b2h
+
+def check_signed(signed: x509.Certificate, signer: x509.Certificate) -> bool:
+ """Verify if 'signed' certificate was signed using 'signer'."""
+ # this code only works for ECDSA, but this is all we need for GSMA eSIM
+ pkey = signer.public_key()
+ # this 'signed.signature_algorithm_parameters' below requires cryptopgraphy 41.0.0 :(
+ pkey.verify(signed.signature, signed.tbs_certificate_bytes, signed.signature_algorithm_parameters)
+
+def cert_get_subject_key_id(cert: x509.Certificate) -> bytes:
+ """Obtain the subject key identifier of the given cert object (as raw bytes)."""
+ ski_ext = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
+ return ski_ext.key_identifier
+
+def cert_get_auth_key_id(cert: x509.Certificate) -> bytes:
+ """Obtain the authority key identifier of the given cert object (as raw bytes)."""
+ aki_ext = cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value
+ return aki_ext.key_identifier
+
+def cert_policy_has_oid(cert: x509.Certificate, match_oid: x509.ObjectIdentifier) -> bool:
+ """Determine if given certificate has a certificatePolicy extension of matching OID."""
+ for policy_ext in filter(lambda x: isinstance(x.value, x509.CertificatePolicies), cert.extensions):
+ if any(policy.policy_identifier == match_oid for policy in policy_ext.value._policies):
+ return True
+ return False
+
+ID_RSP = "2.23.146.1"
+ID_RSP_CERT_OBJECTS = '.'.join([ID_RSP, '2'])
+ID_RSP_ROLE = '.'.join([ID_RSP_CERT_OBJECTS, '1'])
+
+class oid:
+ id_rspRole_ci = x509.ObjectIdentifier(ID_RSP_ROLE + '.0')
+ id_rspRole_euicc_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.1')
+ id_rspRole_eum_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.2')
+ id_rspRole_dp_tls_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.3')
+ id_rspRole_dp_auth_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.4')
+ id_rspRole_dp_pb_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.5')
+ id_rspRole_ds_tls_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.6')
+ id_rspRole_ds_auth_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.7')
+
+class VerifyError(Exception):
+ """An error during certificate verification,"""
+
+class CertificateSet:
+ """A set of certificates consisting of a trusted [self-signed] CA root certificate,
+ and an optional number of intermediate certificates. Can be used to verify the certificate chain
+ of any given other certificate."""
+ def __init__(self, root_cert: x509.Certificate):
+ check_signed(root_cert, root_cert)
+ # TODO: check other mandatory attributes for CA Cert
+ if not cert_policy_has_oid(root_cert, oid.id_rspRole_ci):
+ raise ValueError("Given root certificate doesn't have rspRole_ci OID")
+ usage_ext = root_cert.extensions.get_extension_for_class(x509.KeyUsage).value
+ if not usage_ext.key_cert_sign:
+ raise ValueError('Given root certificate key usage does not permit signing of certificates')
+ if not usage_ext.crl_sign:
+ raise ValueError('Given root certificate key usage does not permit signing of CRLs')
+ self.root_cert = root_cert
+ self.intermediate_certs = {}
+ self.crl = None
+
+ def load_crl(self, urls: Optional[List[str]] = None):
+ if urls and isinstance(urls, str):
+ urls = [urls]
+ if not urls:
+ # generate list of CRL URLs from root CA certificate
+ crl_ext = self.root_cert.extensions.get_extension_for_class(x509.CRLDistributionPoints).value
+ name_list = [x.full_name for x in crl_ext]
+ merged_list = []
+ for n in name_list:
+ merged_list += n
+ uri_list = filter(lambda x: isinstance(x, x509.UniformResourceIdentifier), merged_list)
+ urls = [x.value for x in uri_list]
+
+ for url in urls:
+ try:
+ crl_bytes = requests.get(url, timeout=10)
+ except requests.exceptions.ConnectionError:
+ continue
+ crl = x509.load_der_x509_crl(crl_bytes)
+ if not crl.is_signature_valid(self.root_cert.public_key()):
+ raise ValueError('Given CRL has incorrect signature and cannot be trusted')
+ # FIXME: various other checks
+ self.crl = crl
+ # FIXME: should we support multiple CRLs? we only support a single CRL right now
+ return
+ # FIXME: report on success/failure
+
+ @property
+ def root_cert_id(self) -> bytes:
+ return cert_get_subject_key_id(self.root_cert)
+
+ def add_intermediate_cert(self, cert: x509.Certificate):
+ """Add a potential intermediate certificate to the CertificateSet."""
+ # TODO: check mandatory attributes for intermediate cert
+ usage_ext = cert.extensions.get_extension_for_class(x509.KeyUsage).value
+ if not usage_ext.key_cert_sign:
+ raise ValueError('Given intermediate certificate key usage does not permit signing of certificates')
+ aki = cert_get_auth_key_id(cert)
+ ski = cert_get_subject_key_id(cert)
+ if aki == ski:
+ raise ValueError('Cannot add self-signed cert as intermediate cert')
+ self.intermediate_certs[ski] = cert
+ # TODO: we could test if this cert verifies against the root, and mark it as pre-verified
+ # so we don't need to verify again and again the chain of intermediate certificates
+
+ def verify_cert_crl(self, cert: x509.Certificate):
+ if not self.crl:
+ # we cannot check if there's no CRL
+ return
+ if self.crl.get_revoked_certificate_by_serial_number(cert.serial_nr):
+ raise VerifyError('Certificate is present in CRL, verification failed')
+
+ def verify_cert_chain(self, cert: x509.Certificate, max_depth: int = 100):
+ """Verify if a given certificate's signature chain can be traced back to the root CA of this
+ CertificateSet."""
+ depth = 1
+ c = cert
+ while True:
+ aki = cert_get_auth_key_id(c)
+ if aki == self.root_cert_id:
+ # last step:
+ check_signed(c, self.root_cert)
+ return
+ parent_cert = self.intermediate_certs.get(aki, None)
+ if not aki:
+ raise VerifyError('Could not find intermediate certificate for AuthKeyId %s' % b2h(aki))
+ check_signed(c, parent_cert)
+ # if we reach here, we passed (no exception raised)
+ c = parent_cert
+ depth += 1
+ if depth > max_depth:
+ raise VerifyError('Maximum depth %u exceeded while verifying certificate chain' % max_depth)
+
+
+def ecdsa_dss_to_tr03111(sig: bytes) -> bytes:
+ """convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes."""
+ r, s = decode_dss_signature(sig)
+ return r.to_bytes(32, 'big') + s.to_bytes(32, 'big')
+
+
+class CertAndPrivkey:
+ """A pair of certificate and private key, as used for ECDSA signing."""
+ def __init__(self, required_policy_oid: Optional[x509.ObjectIdentifier] = None,
+ cert: Optional[x509.Certificate] = None, priv_key = None):
+ self.required_policy_oid = required_policy_oid
+ self.cert = cert
+ self.priv_key = priv_key
+
+ def cert_from_der_file(self, path: str):
+ with open(path, 'rb') as f:
+ cert = x509.load_der_x509_certificate(f.read())
+ if self.required_policy_oid:
+ # verify it is the right type of certificate (id-rspRole-dp-auth, id-rspRole-dp-auth-v2, etc.)
+ assert cert_policy_has_oid(cert, self.required_policy_oid)
+ self.cert = cert
+
+ def privkey_from_pem_file(self, path: str, password: Optional[str] = None):
+ with open(path, 'rb') as f:
+ self.priv_key = load_pem_private_key(f.read(), password)
+
+ def ecdsa_sign(self, plaintext: bytes) -> bytes:
+ """Sign some input-data using an ECDSA signature compliant with SGP.22,
+ which internally refers to Global Platform 2.2 Annex E, which in turn points
+ to BSI TS-03111 which states "concatengated raw R + S values". """
+ sig = self.priv_key.sign(plaintext, ec.ECDSA(hashes.SHA256()))
+ # convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes
+ return ecdsa_dss_to_tr03111(sig)
+
+ def get_authority_key_identifier(self) -> x509.AuthorityKeyIdentifier:
+ """Return the AuthorityKeyIdentifier X.509 extension of the certificate."""
+ return list(filter(lambda x: isinstance(x.value, x509.AuthorityKeyIdentifier), self.cert.extensions))[0].value
+
+ def get_subject_alt_name(self) -> x509.SubjectAlternativeName:
+ """Return the SubjectAlternativeName X.509 extension of the certificate."""
+ return list(filter(lambda x: isinstance(x.value, x509.SubjectAlternativeName), self.cert.extensions))[0].value
+
+ def get_cert_as_der(self) -> bytes:
+ """Return certificate encoded as DER."""
+ return self.cert.public_bytes(Encoding.DER)
+
+ def get_curve(self) -> ec.EllipticCurve:
+ return self.cert.public_key().public_numbers().curve
diff --git a/pySim/euicc.py b/pySim/euicc.py
new file mode 100644
index 0000000..ddfe939
--- /dev/null
+++ b/pySim/euicc.py
@@ -0,0 +1,549 @@
+# -*- coding: utf-8 -*-
+
+"""
+Various definitions related to GSMA consumer + IoT eSIM / eUICC
+
+Does *not* implement anything related to M2M eUICC
+
+Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
+"""
+
+# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+
+from construct import Array, Struct, FlagsEnum, GreedyRange
+from cmd2 import cmd2, CommandSet, with_default_category
+
+from pySim.tlv import *
+from pySim.construct import *
+from pySim.commands import SimCardCommands
+from pySim.utils import Hexstr, SwHexstr, SwMatchstr
+import pySim.global_platform
+
+def compute_eid_checksum(eid) -> str:
+ """Compute and add/replace check digits of an EID value according to GSMA SGP.29 Section 10."""
+ if isinstance(eid, str):
+ if len(eid) == 30:
+ # first pad by 2 digits
+ eid += "00"
+ elif len(eid) == 32:
+ # zero the last two digits
+ eid = eid[:-2] + "00"
+ else:
+ raise ValueError("and EID must be 30 or 32 digits")
+ eid_int = int(eid)
+ elif isinstance(eid, int):
+ eid_int = eid
+ if eid_int % 100:
+ # zero the last two digits
+ eid_int -= eid_int % 100
+ # Using the resulting 32 digits as a decimal integer, compute the remainder of that number on division by
+ # 97, Subtract the remainder from 98, and use the decimal result for the two check digits, if the result
+ # is one digit long, its value SHALL be prefixed by one digit of 0.
+ csum = 98 - (eid_int % 97)
+ eid_int += csum
+ return str(eid_int)
+
+def verify_eid_checksum(eid) -> bool:
+ """Verify the check digits of an EID value according to GSMA SGP.29 Section 10."""
+ # Using the 32 digits as a decimal integer, compute the remainder of that number on division by 97. If the
+ # remainder of the division is 1, the verification is successful; otherwise the EID is invalid.
+ return int(eid) % 97 == 1
+
+class VersionAdapter(Adapter):
+ """convert an EUICC Version (3-int array) to a textual representation."""
+
+ def _decode(self, obj, context, path):
+ return "%u.%u.%u" % (obj[0], obj[1], obj[2])
+
+ def _encode(self, obj, context, path):
+ return [int(x) for x in obj.split('.')]
+
+VersionType = VersionAdapter(Array(3, Int8ub))
+
+# Application Identifiers as defined in GSMA SGP.02 Annex H
+AID_ISD_R = "A0000005591010FFFFFFFF8900000100"
+AID_ECASD = "A0000005591010FFFFFFFF8900000200"
+AID_ISD_P_FILE = "A0000005591010FFFFFFFF8900000D00"
+AID_ISD_P_MODULE = "A0000005591010FFFFFFFF8900000E00"
+
+class SupportedVersionNumber(BER_TLV_IE, tag=0x82):
+ _construct = GreedyBytes
+
+class IsdrProprietaryApplicationTemplate(BER_TLV_IE, tag=0xe0, nested=[SupportedVersionNumber]):
+ # FIXME: lpaeSupport - what kind of tag would it have?
+ pass
+
+# GlobalPlatform 2.1.1 Section 9.9.3.1 from pySim/global_platform.py extended with E0
+class FciTemplate(BER_TLV_IE, tag=0x6f, nested=pySim.global_platform.FciTemplateNestedList +
+ [IsdrProprietaryApplicationTemplate]):
+ pass
+
+
+# SGP.22 Section 5.7.3: GetEuiccConfiguredAddresses
+class DefaultDpAddress(BER_TLV_IE, tag=0x80):
+ _construct = Utf8Adapter(GreedyBytes)
+class RootDsAddress(BER_TLV_IE, tag=0x81):
+ _construct = Utf8Adapter(GreedyBytes)
+class EuiccConfiguredAddresses(BER_TLV_IE, tag=0xbf3c, nested=[DefaultDpAddress, RootDsAddress]):
+ pass
+
+# SGP.22 Section 5.7.4: SetDefaultDpAddress
+class SetDefaultDpAddrRes(BER_TLV_IE, tag=0x80):
+ _construct = Enum(Int8ub, ok=0, undefinedError=127)
+class SetDefaultDpAddress(BER_TLV_IE, tag=0xbf3f, nested=[DefaultDpAddress, SetDefaultDpAddrRes]):
+ pass
+
+# SGP.22 Section 5.7.7: GetEUICCChallenge
+class EuiccChallenge(BER_TLV_IE, tag=0x80):
+ _construct = HexAdapter(Bytes(16))
+class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
+ pass
+
+# SGP.22 Section 5.7.8: GetEUICCInfo
+class SVN(BER_TLV_IE, tag=0x82):
+ _construct = VersionType
+class SubjectKeyIdentifier(BER_TLV_IE, tag=0x04):
+ _construct = HexAdapter(GreedyBytes)
+class EuiccCiPkiListForVerification(BER_TLV_IE, tag=0xa9, nested=[SubjectKeyIdentifier]):
+ pass
+class EuiccCiPkiListForSigning(BER_TLV_IE, tag=0xaa, nested=[SubjectKeyIdentifier]):
+ pass
+class EuiccInfo1(BER_TLV_IE, tag=0xbf20, nested=[SVN, EuiccCiPkiListForVerification, EuiccCiPkiListForSigning]):
+ pass
+class ProfileVersion(BER_TLV_IE, tag=0x81):
+ _construct = VersionType
+class EuiccFirmwareVer(BER_TLV_IE, tag=0x83):
+ _construct = VersionType
+class ExtCardResource(BER_TLV_IE, tag=0x84):
+ _construct = HexAdapter(GreedyBytes)
+class UiccCapability(BER_TLV_IE, tag=0x85):
+ _construct = HexAdapter(GreedyBytes) # FIXME
+class TS102241Version(BER_TLV_IE, tag=0x86):
+ _construct = VersionType
+class GlobalPlatformVersion(BER_TLV_IE, tag=0x87):
+ _construct = VersionType
+class RspCapability(BER_TLV_IE, tag=0x88):
+ _construct = HexAdapter(GreedyBytes) # FIXME
+class EuiccCategory(BER_TLV_IE, tag=0x8b):
+ _construct = Enum(Int8ub, other=0, basicEuicc=1, mediumEuicc=2, contactlessEuicc=3)
+class PpVersion(BER_TLV_IE, tag=0x04):
+ _construct = VersionType
+class SsAcreditationNumber(BER_TLV_IE, tag=0x0c):
+ _construct = Utf8Adapter(GreedyBytes)
+class IpaMode(BER_TLV_IE, tag=0x90): # see SGP.32 v1.0
+ _construct = Enum(Int8ub, ipad=0, ipea=1)
+class IotVersion(BER_TLV_IE, tag=0x80): # see SGP.32 v1.0
+ _construct = VersionType
+class IotVersionSeq(BER_TLV_IE, tag=0xa0, nested=[IotVersion]): # see SGP.32 v1.0
+ pass
+class IotSpecificInfo(BER_TLV_IE, tag=0x94, nested=[IotVersionSeq]): # see SGP.32 v1.0
+ pass
+class EuiccInfo2(BER_TLV_IE, tag=0xbf22, nested=[ProfileVersion, SVN, EuiccFirmwareVer, ExtCardResource,
+ UiccCapability, TS102241Version, GlobalPlatformVersion,
+ RspCapability, EuiccCiPkiListForVerification,
+ EuiccCiPkiListForSigning, EuiccCategory, PpVersion,
+ SsAcreditationNumber, IpaMode, IotSpecificInfo]):
+ pass
+
+# SGP.22 Section 5.7.9: ListNotification
+class ProfileMgmtOperation(BER_TLV_IE, tag=0x81):
+ # we have to ignore the first byte which tells us how many padding bits are used in the last octet
+ _construct = Struct(Byte, "pmo"/FlagsEnum(Byte, install=0x80, enable=0x40, disable=0x20, delete=0x10))
+class ListNotificationReq(BER_TLV_IE, tag=0xbf28, nested=[ProfileMgmtOperation]):
+ pass
+class SeqNumber(BER_TLV_IE, tag=0x80):
+ _construct = GreedyInteger()
+class NotificationAddress(BER_TLV_IE, tag=0x0c):
+ _construct = Utf8Adapter(GreedyBytes)
+class Iccid(BER_TLV_IE, tag=0x5a):
+ _construct = BcdAdapter(GreedyBytes)
+class NotificationMetadata(BER_TLV_IE, tag=0xbf2f, nested=[SeqNumber, ProfileMgmtOperation,
+ NotificationAddress, Iccid]):
+ pass
+class NotificationMetadataList(BER_TLV_IE, tag=0xa0, nested=[NotificationMetadata]):
+ pass
+class ListNotificationsResultError(BER_TLV_IE, tag=0x81):
+ _construct = Enum(Int8ub, undefinedError=127)
+class ListNotificationResp(BER_TLV_IE, tag=0xbf28, nested=[NotificationMetadataList,
+ ListNotificationsResultError]):
+ pass
+
+# SGP.22 Section 5.7.11: RemoveNotificationFromList
+class DeleteNotificationStatus(BER_TLV_IE, tag=0x80):
+ _construct = Enum(Int8ub, ok=0, nothingToDelete=1, undefinedError=127)
+class NotificationSentReq(BER_TLV_IE, tag=0xbf30, nested=[SeqNumber]):
+ pass
+class NotificationSentResp(BER_TLV_IE, tag=0xbf30, nested=[DeleteNotificationStatus]):
+ pass
+
+# SGP.22 Section 5.7.12: LoadCRL: FIXME
+class LoadCRL(BER_TLV_IE, tag=0xbf35, nested=[]): # FIXME
+ pass
+
+# SGP.22 Section 5.7.15: GetProfilesInfo
+class TagList(BER_TLV_IE, tag=0x5c):
+ _construct = GreedyRange(Int8ub) # FIXME: tags could be multi-byte
+class ProfileInfoListReq(BER_TLV_IE, tag=0xbf2d, nested=[TagList]): # FIXME: SearchCriteria
+ pass
+class IsdpAid(BER_TLV_IE, tag=0x4f):
+ _construct = HexAdapter(GreedyBytes)
+class ProfileState(BER_TLV_IE, tag=0x9f70):
+ _construct = Enum(Int8ub, disabled=0, enabled=1)
+class ProfileNickname(BER_TLV_IE, tag=0x90):
+ _construct = Utf8Adapter(GreedyBytes)
+class ServiceProviderName(BER_TLV_IE, tag=0x91):
+ _construct = Utf8Adapter(GreedyBytes)
+class ProfileName(BER_TLV_IE, tag=0x92):
+ _construct = Utf8Adapter(GreedyBytes)
+class IconType(BER_TLV_IE, tag=0x93):
+ _construct = Enum(Int8ub, jpg=0, png=1)
+class Icon(BER_TLV_IE, tag=0x94):
+ _construct = GreedyBytes
+class ProfileClass(BER_TLV_IE, tag=0x95):
+ _construct = Enum(Int8ub, test=0, provisioning=1, operational=2)
+class ProfileInfo(BER_TLV_IE, tag=0xe3, nested=[Iccid, IsdpAid, ProfileState, ProfileNickname,
+ ServiceProviderName, ProfileName, IconType, Icon,
+ ProfileClass]): # FIXME: more IEs
+ pass
+class ProfileInfoSeq(BER_TLV_IE, tag=0xa0, nested=[ProfileInfo]):
+ pass
+class ProfileInfoListError(BER_TLV_IE, tag=0x81):
+ _construct = Enum(Int8ub, incorrectInputValues=1, undefinedError=2)
+class ProfileInfoListResp(BER_TLV_IE, tag=0xbf2d, nested=[ProfileInfoSeq, ProfileInfoListError]):
+ pass
+
+# SGP.22 Section 5.7.16:: EnableProfile
+class RefreshFlag(BER_TLV_IE, tag=0x81): # FIXME
+ _construct = Int8ub # FIXME
+class EnableResult(BER_TLV_IE, tag=0x80):
+ _construct = Enum(Int8ub, ok=0, iccidOrAidNotFound=1, profileNotInDisabledState=2,
+ disallowedByPolicy=3, wrongProfileReenabling=4, catBusy=5, undefinedError=127)
+class ProfileIdentifier(BER_TLV_IE, tag=0xa0, nested=[IsdpAid, Iccid]):
+ pass
+class EnableProfileReq(BER_TLV_IE, tag=0xbf31, nested=[ProfileIdentifier, RefreshFlag]):
+ pass
+class EnableProfileResp(BER_TLV_IE, tag=0xbf31, nested=[EnableResult]):
+ pass
+
+# SGP.22 Section 5.7.17 DisableProfile
+class DisableResult(BER_TLV_IE, tag=0x80):
+ _construct = Enum(Int8ub, ok=0, iccidOrAidNotFound=1, profileNotInEnabledState=2,
+ disallowedByPolicy=3, catBusy=5, undefinedError=127)
+class DisableProfileReq(BER_TLV_IE, tag=0xbf32, nested=[ProfileIdentifier, RefreshFlag]):
+ pass
+class DisableProfileResp(BER_TLV_IE, tag=0xbf32, nested=[DisableResult]):
+ pass
+
+# SGP.22 Section 5.7.18: DeleteProfile
+class DeleteResult(BER_TLV_IE, tag=0x80):
+ _construct = Enum(Int8ub, ok=0, iccidOrAidNotFound=1, profileNotInDisabledState=2,
+ disallowedByPolicy=3, undefinedError=127)
+class DeleteProfileReq(BER_TLV_IE, tag=0xbf33, nested=[IsdpAid, Iccid]):
+ pass
+class DeleteProfileResp(BER_TLV_IE, tag=0xbf33, nested=[DeleteResult]):
+ pass
+
+# SGP.22 Section 5.7.20 GetEID
+class EidValue(BER_TLV_IE, tag=0x5a):
+ _construct = HexAdapter(GreedyBytes)
+class GetEuiccData(BER_TLV_IE, tag=0xbf3e, nested=[TagList, EidValue]):
+ pass
+
+# SGP.22 Section 5.7.21: ES10c SetNickname
+class SnrProfileNickname(BER_TLV_IE, tag=0x8f):
+ _construct = Utf8Adapter(GreedyBytes)
+class SetNicknameReq(BER_TLV_IE, tag=0xbf29, nested=[Iccid, SnrProfileNickname]):
+ pass
+class SetNicknameResult(BER_TLV_IE, tag=0x80):
+ _construct = Enum(Int8ub, ok=0, iccidNotFound=1, undefinedError=127)
+class SetNicknameResp(BER_TLV_IE, tag=0xbf29, nested=[SetNicknameResult]):
+ pass
+
+# SGP.32 Section 5.9.10: ES10b: GetCerts
+class GetCertsReq(BER_TLV_IE, tag=0xbf56):
+ pass
+class EumCertificate(BER_TLV_IE, tag=0xa5):
+ _construct = GreedyBytes
+class EuiccCertificate(BER_TLV_IE, tag=0xa6):
+ _construct = GreedyBytes
+class GetCertsError(BER_TLV_IE, tag=0x81):
+ _construct = Enum(Int8ub, invalidCiPKId=1, undefinedError=127)
+class GetCertsResp(BER_TLV_IE, tag=0xbf56, nested=[EumCertificate, EuiccCertificate, GetCertsError]):
+ pass
+
+# SGP.32 Section 5.9.18: ES10b: GetEimConfigurationData
+class EimId(BER_TLV_IE, tag=0x80):
+ _construct = Utf8Adapter(GreedyBytes)
+class EimFqdn(BER_TLV_IE, tag=0x81):
+ _construct = Utf8Adapter(GreedyBytes)
+class EimIdType(BER_TLV_IE, tag=0x82):
+ _construct = Enum(Int8ub, eimIdTypeOid=1, eimIdTypeFqdn=2, eimIdTypeProprietary=3)
+class CounterValue(BER_TLV_IE, tag=0x83):
+ _construct = GreedyInteger
+class AssociationToken(BER_TLV_IE, tag=0x84):
+ _construct = GreedyInteger
+class EimSupportedProtocol(BER_TLV_IE, tag=0x87):
+ _construct = Enum(Int8ub, eimRetrieveHttps=0, eimRetrieveCoaps=1, eimInjectHttps=2, eimInjectCoaps=3,
+ eimProprietary=4)
+# FIXME: eimPublicKeyData, trustedPublicKeyDataTls, euiccCiPKId
+class EimConfigurationData(BER_TLV_IE, tag=0x80, nested=[EimId, EimFqdn, EimIdType, CounterValue,
+ AssociationToken, EimSupportedProtocol]):
+ pass
+class EimConfigurationDataSeq(BER_TLV_IE, tag=0xa0, nested=[EimConfigurationData]):
+ pass
+class GetEimConfigurationData(BER_TLV_IE, tag=0xbf55, nested=[EimConfigurationDataSeq]):
+ pass
+
+class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
+ def __init__(self):
+ super().__init__(name='ADF.ISD-R', aid=AID_ISD_R,
+ desc='ISD-R (Issuer Security Domain Root) Application')
+ self.adf.decode_select_response = self.decode_select_response
+ self.adf.shell_commands += [self.AddlShellCommands()]
+ # we attempt to retrieve ISD-R key material from CardKeyProvider identified by EID
+ self.adf.scp_key_identity = 'EID'
+
+ @staticmethod
+ def store_data(scc: SimCardCommands, tx_do: Hexstr, exp_sw: SwMatchstr ="9000") -> Tuple[Hexstr, SwHexstr]:
+ """Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22.
+ Only single-block store supported for now."""
+ capdu = '%sE29100%02x%s' % (scc.cla4lchan('80'), len(tx_do)//2, tx_do)
+ return scc.send_apdu_checksw(capdu, exp_sw)
+
+ @staticmethod
+ def store_data_tlv(scc: SimCardCommands, cmd_do, resp_cls, exp_sw: SwMatchstr = '9000'):
+ """Transceive STORE DATA APDU with the card, transparently encoding the command data from TLV
+ and decoding the response data tlv."""
+ if cmd_do:
+ cmd_do_enc = cmd_do.to_tlv()
+ cmd_do_len = len(cmd_do_enc)
+ if cmd_do_len > 255:
+ return ValueError('DO > 255 bytes not supported yet')
+ else:
+ cmd_do_enc = b''
+ (data, _sw) = CardApplicationISDR.store_data(scc, b2h(cmd_do_enc), exp_sw=exp_sw)
+ if data:
+ if resp_cls:
+ resp_do = resp_cls()
+ resp_do.from_tlv(h2b(data))
+ return resp_do
+ else:
+ return data
+ else:
+ return None
+
+ @staticmethod
+ def get_eid(scc: SimCardCommands) -> str:
+ ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
+ ged = CardApplicationISDR.store_data_tlv(scc, ged_cmd, GetEuiccData)
+ d = ged.to_dict()
+ return flatten_dict_lists(d['get_euicc_data'])['eid_value']
+
+ def decode_select_response(self, data_hex: Hexstr) -> object:
+ t = FciTemplate()
+ t.from_tlv(h2b(data_hex))
+ d = t.to_dict()
+ return flatten_dict_lists(d['fci_template'])
+
+ @with_default_category('Application-Specific Commands')
+ class AddlShellCommands(CommandSet):
+
+ es10x_store_data_parser = argparse.ArgumentParser()
+ es10x_store_data_parser.add_argument('TX_DO', help='Hexstring of encoded to-be-transmitted DO')
+
+ @cmd2.with_argparser(es10x_store_data_parser)
+ def do_es10x_store_data(self, opts):
+ """Perform a raw STORE DATA command as defined for the ES10x eUICC interface."""
+ (_data, _sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, opts.TX_DO)
+
+ def do_get_euicc_configured_addresses(self, _opts):
+ """Perform an ES10a GetEuiccConfiguredAddresses function."""
+ eca = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccConfiguredAddresses(), EuiccConfiguredAddresses)
+ d = eca.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['euicc_configured_addresses']))
+
+ set_def_dp_addr_parser = argparse.ArgumentParser()
+ set_def_dp_addr_parser.add_argument('DP_ADDRESS', help='Default SM-DP+ address as UTF-8 string')
+
+ @cmd2.with_argparser(set_def_dp_addr_parser)
+ def do_set_default_dp_address(self, opts):
+ """Perform an ES10a SetDefaultDpAddress function."""
+ sdda_cmd = SetDefaultDpAddress(children=[DefaultDpAddress(decoded=opts.DP_ADDRESS)])
+ sdda = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sdda_cmd, SetDefaultDpAddress)
+ d = sdda.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['set_default_dp_address']))
+
+ def do_get_euicc_challenge(self, _opts):
+ """Perform an ES10b GetEUICCChallenge function."""
+ gec = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEuiccChallenge(), GetEuiccChallenge)
+ d = gec.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_challenge']))
+
+ def do_get_euicc_info1(self, _opts):
+ """Perform an ES10b GetEUICCInfo (1) function."""
+ ei1 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo1(), EuiccInfo1)
+ d = ei1.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['euicc_info1']))
+
+ def do_get_euicc_info2(self, _opts):
+ """Perform an ES10b GetEUICCInfo (2) function."""
+ ei2 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo2(), EuiccInfo2)
+ d = ei2.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['euicc_info2']))
+
+ def do_list_notification(self, _opts):
+ """Perform an ES10b ListNotification function."""
+ ln = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
+ d = ln.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['list_notification_resp']))
+
+ rem_notif_parser = argparse.ArgumentParser()
+ rem_notif_parser.add_argument('SEQ_NR', type=int, help='Sequence Number of the to-be-removed notification')
+
+ @cmd2.with_argparser(rem_notif_parser)
+ def do_remove_notification_from_list(self, opts):
+ """Perform an ES10b RemoveNotificationFromList function."""
+ rn_cmd = NotificationSentReq(children=[SeqNumber(decoded=opts.SEQ_NR)])
+ rn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, rn_cmd, NotificationSentResp)
+ d = rn.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['notification_sent_resp']))
+
+ def do_get_profiles_info(self, _opts):
+ """Perform an ES10c GetProfilesInfo function."""
+ pi = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ProfileInfoListReq(), ProfileInfoListResp)
+ d = pi.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['profile_info_list_resp']))
+
+ en_prof_parser = argparse.ArgumentParser()
+ en_prof_grp = en_prof_parser.add_mutually_exclusive_group()
+ en_prof_grp.add_argument('--isdp-aid', help='Profile identified by its ISD-P AID')
+ en_prof_grp.add_argument('--iccid', help='Profile identified by its ICCID')
+ en_prof_parser.add_argument('--refresh-required', action='store_true', help='whether a REFRESH is required')
+
+ @cmd2.with_argparser(en_prof_parser)
+ def do_enable_profile(self, opts):
+ """Perform an ES10c EnableProfile function."""
+ if opts.isdp_aid:
+ p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
+ elif opts.iccid:
+ p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
+ else:
+ # this is guaranteed by argparse; but we need this to make pylint happy
+ raise ValueError('Either ISD-P AID or ICCID must be given')
+ ep_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
+ ep_cmd = EnableProfileReq(children=ep_cmd_contents)
+ ep = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ep_cmd, EnableProfileResp)
+ d = ep.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['enable_profile_resp']))
+
+ dis_prof_parser = argparse.ArgumentParser()
+ dis_prof_grp = dis_prof_parser.add_mutually_exclusive_group()
+ dis_prof_grp.add_argument('--isdp-aid', help='Profile identified by its ISD-P AID')
+ dis_prof_grp.add_argument('--iccid', help='Profile identified by its ICCID')
+ dis_prof_parser.add_argument('--refresh-required', action='store_true', help='whether a REFRESH is required')
+
+ @cmd2.with_argparser(dis_prof_parser)
+ def do_disable_profile(self, opts):
+ """Perform an ES10c DisableProfile function."""
+ if opts.isdp_aid:
+ p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
+ elif opts.iccid:
+ p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
+ else:
+ # this is guaranteed by argparse; but we need this to make pylint happy
+ raise ValueError('Either ISD-P AID or ICCID must be given')
+ dp_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
+ dp_cmd = DisableProfileReq(children=dp_cmd_contents)
+ dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DisableProfileResp)
+ d = dp.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['disable_profile_resp']))
+
+ del_prof_parser = argparse.ArgumentParser()
+ del_prof_grp = del_prof_parser.add_mutually_exclusive_group()
+ del_prof_grp.add_argument('--isdp-aid', help='Profile identified by its ISD-P AID')
+ del_prof_grp.add_argument('--iccid', help='Profile identified by its ICCID')
+
+ @cmd2.with_argparser(del_prof_parser)
+ def do_delete_profile(self, opts):
+ """Perform an ES10c DeleteProfile function."""
+ if opts.isdp_aid:
+ p_id = IsdpAid(decoded=opts.isdp_aid)
+ elif opts.iccid:
+ p_id = Iccid(decoded=opts.iccid)
+ else:
+ # this is guaranteed by argparse; but we need this to make pylint happy
+ raise ValueError('Either ISD-P AID or ICCID must be given')
+ dp_cmd_contents = [p_id]
+ dp_cmd = DeleteProfileReq(children=dp_cmd_contents)
+ dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DeleteProfileResp)
+ d = dp.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['delete_profile_resp']))
+
+
+ def do_get_eid(self, _opts):
+ """Perform an ES10c GetEID function."""
+ ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
+ ged = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
+ d = ged.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_data']))
+
+ set_nickname_parser = argparse.ArgumentParser()
+ set_nickname_parser.add_argument('ICCID', help='ICCID of the profile whose nickname to set')
+ set_nickname_parser.add_argument('--profile-nickname', help='Nickname of the profile')
+
+ @cmd2.with_argparser(set_nickname_parser)
+ def do_set_nickname(self, opts):
+ """Perform an ES10c SetNickname function."""
+ nickname = opts.profile_nickname or ''
+ sn_cmd_contents = [Iccid(decoded=opts.ICCID), ProfileNickname(decoded=nickname)]
+ sn_cmd = SetNicknameReq(children=sn_cmd_contents)
+ sn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sn_cmd, SetNicknameResp)
+ d = sn.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['set_nickname_resp']))
+
+ def do_get_certs(self, _opts):
+ """Perform an ES10c GetCerts() function on an IoT eUICC."""
+ gc = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
+ d = gc.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['get_certs_resp']))
+
+ def do_get_eim_configuration_data(self, _opts):
+ """Perform an ES10b GetEimConfigurationData function on an Iot eUICC."""
+ gec = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
+ GetEimConfigurationData)
+ d = gec.to_dict()
+ self._cmd.poutput_json(flatten_dict_lists(d['get_eim_configuration_data']))
+
+class CardApplicationECASD(pySim.global_platform.CardApplicationSD):
+ def decode_select_response(self, data_hex: Hexstr) -> object:
+ t = FciTemplate()
+ t.from_tlv(h2b(data_hex))
+ d = t.to_dict()
+ return flatten_dict_lists(d['fci_template'])
+
+ def __init__(self):
+ super().__init__(name='ADF.ECASD', aid=AID_ECASD,
+ desc='ECASD (eUICC Controlling Authority Security Domain) Application')
+ self.adf.decode_select_response = self.decode_select_response
+ self.adf.shell_commands += [self.AddlShellCommands()]
+ # we attempt to retrieve ECASD key material from CardKeyProvider identified by EID
+ self.adf.scp_key_identity = 'EID'
+
+ @with_default_category('Application-Specific Commands')
+ class AddlShellCommands(CommandSet):
+ pass
diff --git a/pySim/exceptions.py b/pySim/exceptions.py
index c6a81a1..f726aa6 100644
--- a/pySim/exceptions.py
+++ b/pySim/exceptions.py
@@ -24,17 +24,14 @@
class NoCardError(Exception):
"""No card was found in the reader."""
- pass
class ProtocolError(Exception):
"""Some kind of protocol level error interfacing with the card."""
- pass
class ReaderError(Exception):
"""Some kind of general error with the card reader."""
- pass
class SwMatchError(Exception):
diff --git a/pySim/filesystem.py b/pySim/filesystem.py
index 04e849b..fb5d680 100644
--- a/pySim/filesystem.py
+++ b/pySim/filesystem.py
@@ -24,24 +24,20 @@ not the actual contents / runtime state of interacting with a given smart card.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import code
+from typing import cast, Optional, Iterable, List, Dict, Tuple, Union
+import argparse
import tempfile
import json
import abc
import inspect
import cmd2
-from cmd2 import CommandSet, with_default_category, with_argparser
-import argparse
-
-from typing import cast, Optional, Iterable, List, Dict, Tuple, Union
-
+from cmd2 import CommandSet, with_default_category
from smartcard.util import toBytes
-from pySim.utils import sw_match, h2b, b2h, i2h, is_hex, auto_int, bertlv_parse_one, Hexstr
-from pySim.construct import filter_dict, parse_construct
-from pySim.exceptions import *
-from pySim.jsonpath import js_path_find, js_path_modify
+from pySim.utils import sw_match, h2b, b2h, is_hex, auto_int, auto_uint8, auto_uint16, is_hexstr
+from pySim.construct import filter_dict, parse_construct, build_construct
+from pySim.jsonpath import js_path_modify
from pySim.commands import SimCardCommands
# int: a single service is associated with this file
@@ -51,18 +47,6 @@ CardFileService = Union[int, List[int], Tuple[int, ...]]
Size = Tuple[int, Optional[int]]
-def lchan_nr_from_cla(cla: int) -> int:
- """Resolve the logical channel number from the CLA byte."""
- # TS 102 221 10.1.1 Coding of Class Byte
- if cla >> 4 in [0x0, 0xA, 0x8]:
- # Table 10.3
- return cla & 0x03
- elif cla & 0xD0 in [0x40, 0xC0]:
- # Table 10.4a
- return 4 + (cla & 0x0F)
- else:
- raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
-
class CardFile:
"""Base class for all objects in the smart card filesystem.
Serve as a common ancestor to all other file types; rarely used directly.
@@ -83,7 +67,7 @@ class CardFile:
profile : Card profile that this file should be part of
service : Service (SST/UST/IST) associated with the file
"""
- if not isinstance(self, CardADF) and fid == None:
+ if not isinstance(self, CardADF) and fid is None:
raise ValueError("fid is mandatory")
if fid:
fid = fid.lower()
@@ -149,10 +133,22 @@ class CardFile:
def build_select_path_to(self, target: 'CardFile') -> Optional[List['CardFile']]:
"""Build the relative sequence of files we need to traverse to get from us to 'target'."""
+
+ # special-case handling for applications. Applications may be selected
+ # any time from any location. If there is an ADF somewhere in the path,
+ # we may clip everything before that ADF.
+ def clip_path(inter_path):
+ for i in reversed(range(0, len(inter_path))):
+ if isinstance(inter_path[i], CardADF):
+ return inter_path[i:]
+ return inter_path
+
+ # special-case handling for selecting MF while the MF is selected
+ if target == target.get_mf():
+ return [target]
cur_fqpath = self.fully_qualified_path_fobj()
target_fqpath = target.fully_qualified_path_fobj()
inter_path = []
- cur_fqpath.pop() # drop last element (currently selected file, doesn't need re-selection
cur_fqpath.reverse()
for ce in cur_fqpath:
inter_path.append(ce)
@@ -162,12 +158,12 @@ class CardFile:
for te2 in target_fqpath[i+1:]:
inter_path.append(te2)
# we found our common ancestor
- return inter_path
+ return clip_path(inter_path[1:])
return None
def get_mf(self) -> Optional['CardMF']:
"""Return the MF (root) of the file system."""
- if self.parent == None:
+ if self.parent is None:
return None
# iterate towards the top. MF has parent == self
node = self
@@ -282,23 +278,22 @@ class CardFile:
"""Assuming the provided list of activated services, should this file exist and be activated?."""
if self.service is None:
return None
- elif isinstance(self.service, int):
+ if isinstance(self.service, int):
# a single service determines the result
return self.service in services
- elif isinstance(self.service, list):
+ if isinstance(self.service, list):
# any of the services active -> true
for s in self.service:
if s in services:
return True
return False
- elif isinstance(self.service, tuple):
+ if isinstance(self.service, tuple):
# all of the services active -> true
for s in self.service:
if not s in services:
return False
return True
- else:
- raise ValueError("self.service must be either int or list or tuple")
+ raise ValueError("self.service must be either int or list or tuple")
class CardDF(CardFile):
@@ -306,15 +301,14 @@ class CardDF(CardFile):
@with_default_category('DF/ADF Commands')
class ShellCommands(CommandSet):
- def __init__(self):
- super().__init__()
+ pass
def __init__(self, **kwargs):
if not isinstance(self, CardADF):
- if not 'fid' in kwargs:
+ if 'fid' not in kwargs:
raise TypeError('fid is mandatory for all DF')
super().__init__(**kwargs)
- self.children = dict()
+ self.children = {}
self.shell_commands = [self.ShellCommands()]
# dict of CardFile affected by service(int), indexed by service
self.files_by_service = {}
@@ -415,7 +409,7 @@ class CardDF(CardFile):
def lookup_file_by_name(self, name: Optional[str]) -> Optional[CardFile]:
"""Find a file with given name within current DF."""
- if name == None:
+ if name is None:
return None
for i in self.children.values():
if i.name and i.name == name:
@@ -424,7 +418,7 @@ class CardDF(CardFile):
def lookup_file_by_sfid(self, sfid: Optional[str]) -> Optional[CardFile]:
"""Find a file with given short file ID within current DF."""
- if sfid == None:
+ if sfid is None:
return None
for i in self.children.values():
if i.sfid == int(str(sfid)):
@@ -449,7 +443,7 @@ class CardMF(CardDF):
# cannot be overridden; use assignment
kwargs['parent'] = self
super().__init__(**kwargs)
- self.applications = dict()
+ self.applications = {}
def __str__(self):
return "MF(%s)" % (self.fid)
@@ -514,11 +508,12 @@ class CardMF(CardDF):
class CardADF(CardDF):
"""ADF (Application Dedicated File) in the smart card filesystem"""
- def __init__(self, aid: str, **kwargs):
+ def __init__(self, aid: str, has_fs: bool=False, **kwargs):
super().__init__(**kwargs)
# reference to CardApplication may be set from CardApplication constructor
self.application = None # type: Optional[CardApplication]
- self.aid = aid # Application Identifier
+ self.aid = aid.lower() # Application Identifier
+ self.has_fs = has_fs # Flag to tell whether the ADF supports a filesystem or not
mf = self.get_mf()
if mf:
mf.add_application_df(self)
@@ -572,13 +567,10 @@ class TransparentEF(CardEF):
class ShellCommands(CommandSet):
"""Shell commands specific for transparent EFs."""
- def __init__(self):
- super().__init__()
-
dec_hex_parser = argparse.ArgumentParser()
dec_hex_parser.add_argument('--oneline', action='store_true',
help='No JSON pretty-printing, dump as a single line')
- dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
+ dec_hex_parser.add_argument('HEXSTR', type=is_hexstr, help='Hex-string of encoded data to decode')
@cmd2.with_argparser(dec_hex_parser)
def do_decode_hex(self, opts):
@@ -588,14 +580,14 @@ class TransparentEF(CardEF):
read_bin_parser = argparse.ArgumentParser()
read_bin_parser.add_argument(
- '--offset', type=int, default=0, help='Byte offset for start of read')
+ '--offset', type=auto_uint16, default=0, help='Byte offset for start of read')
read_bin_parser.add_argument(
- '--length', type=int, help='Number of bytes to read')
+ '--length', type=auto_uint16, help='Number of bytes to read')
@cmd2.with_argparser(read_bin_parser)
def do_read_binary(self, opts):
"""Read binary data from a transparent EF"""
- (data, sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
+ (data, _sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
self._cmd.poutput(data)
read_bin_dec_parser = argparse.ArgumentParser()
@@ -605,25 +597,23 @@ class TransparentEF(CardEF):
@cmd2.with_argparser(read_bin_dec_parser)
def do_read_binary_decoded(self, opts):
"""Read + decode data from a transparent EF"""
- (data, sw) = self._cmd.lchan.read_binary_dec()
+ (data, _sw) = self._cmd.lchan.read_binary_dec()
self._cmd.poutput_json(data, opts.oneline)
upd_bin_parser = argparse.ArgumentParser()
upd_bin_parser.add_argument(
- '--offset', type=int, default=0, help='Byte offset for start of read')
- upd_bin_parser.add_argument(
- 'data', help='Data bytes (hex format) to write')
+ '--offset', type=auto_uint16, default=0, help='Byte offset for start of read')
+ upd_bin_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
@cmd2.with_argparser(upd_bin_parser)
def do_update_binary(self, opts):
"""Update (Write) data of a transparent EF"""
- (data, sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
+ (data, _sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
if data:
self._cmd.poutput(data)
upd_bin_dec_parser = argparse.ArgumentParser()
- upd_bin_dec_parser.add_argument(
- 'data', help='Abstract data (JSON format) to write')
+ upd_bin_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
upd_bin_dec_parser.add_argument('--json-path', type=str,
help='JSON path to modify specific element of file only')
@@ -631,18 +621,18 @@ class TransparentEF(CardEF):
def do_update_binary_decoded(self, opts):
"""Encode + Update (Write) data of a transparent EF"""
if opts.json_path:
- (data_json, sw) = self._cmd.lchan.read_binary_dec()
+ (data_json, _sw) = self._cmd.lchan.read_binary_dec()
js_path_modify(data_json, opts.json_path,
json.loads(opts.data))
else:
data_json = json.loads(opts.data)
- (data, sw) = self._cmd.lchan.update_binary_dec(data_json)
+ (data, _sw) = self._cmd.lchan.update_binary_dec(data_json)
if data:
self._cmd.poutput_json(data)
- def do_edit_binary_decoded(self, opts):
+ def do_edit_binary_decoded(self, _opts):
"""Edit the JSON representation of the EF contents in an editor."""
- (orig_json, sw) = self._cmd.lchan.read_binary_dec()
+ (orig_json, _sw) = self._cmd.lchan.read_binary_dec()
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
filename = '%s/file' % dirname
# write existing data as JSON to file
@@ -655,7 +645,7 @@ class TransparentEF(CardEF):
if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write")
else:
- (data, sw) = self._cmd.lchan.update_binary_dec(edited_json)
+ (data, _sw) = self._cmd.lchan.update_binary_dec(edited_json)
if data:
self._cmd.poutput_json(data)
@@ -696,7 +686,7 @@ class TransparentEF(CardEF):
return method(b2h(raw_bin_data))
if self._construct:
return parse_construct(self._construct, raw_bin_data)
- elif self._tlv:
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_tlv(raw_bin_data)
return t.to_dict()
@@ -723,7 +713,7 @@ class TransparentEF(CardEF):
return method(raw_bin_data)
if self._construct:
return parse_construct(self._construct, raw_bin_data)
- elif self._tlv:
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_tlv(raw_bin_data)
return t.to_dict()
@@ -748,8 +738,8 @@ class TransparentEF(CardEF):
if callable(method):
return h2b(method(abstract_data))
if self._construct:
- return self._construct.build(abstract_data)
- elif self._tlv:
+ return build_construct(self._construct, abstract_data)
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return t.to_tlv()
@@ -776,8 +766,8 @@ class TransparentEF(CardEF):
raw_bin_data = method(abstract_data)
return b2h(raw_bin_data)
if self._construct:
- return b2h(self._construct.build(abstract_data))
- elif self._tlv:
+ return b2h(build_construct(self._construct, abstract_data))
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return b2h(t.to_tlv())
@@ -794,14 +784,10 @@ class LinFixedEF(CardEF):
@with_default_category('Linear Fixed EF Commands')
class ShellCommands(CommandSet):
"""Shell commands specific for Linear Fixed EFs."""
-
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
-
dec_hex_parser = argparse.ArgumentParser()
dec_hex_parser.add_argument('--oneline', action='store_true',
help='No JSON pretty-printing, dump as a single line')
- dec_hex_parser.add_argument('HEXSTR', help='Hex-string of encoded data to decode')
+ dec_hex_parser.add_argument('HEXSTR', type=is_hexstr, help='Hex-string of encoded data to decode')
@cmd2.with_argparser(dec_hex_parser)
def do_decode_hex(self, opts):
@@ -811,17 +797,17 @@ class LinFixedEF(CardEF):
read_rec_parser = argparse.ArgumentParser()
read_rec_parser.add_argument(
- 'record_nr', type=int, help='Number of record to be read')
+ 'record_nr', type=auto_uint8, help='Number of record to be read')
read_rec_parser.add_argument(
- '--count', type=int, default=1, help='Number of records to be read, beginning at record_nr')
+ '--count', type=auto_uint8, default=1, help='Number of records to be read, beginning at record_nr')
@cmd2.with_argparser(read_rec_parser)
def do_read_record(self, opts):
"""Read one or multiple records from a record-oriented EF"""
for r in range(opts.count):
recnr = opts.record_nr + r
- (data, sw) = self._cmd.lchan.read_record(recnr)
- if (len(data) > 0):
+ (data, _sw) = self._cmd.lchan.read_record(recnr)
+ if len(data) > 0:
recstr = str(data)
else:
recstr = "(empty)"
@@ -829,25 +815,25 @@ class LinFixedEF(CardEF):
read_rec_dec_parser = argparse.ArgumentParser()
read_rec_dec_parser.add_argument(
- 'record_nr', type=int, help='Number of record to be read')
+ 'record_nr', type=auto_uint8, help='Number of record to be read')
read_rec_dec_parser.add_argument('--oneline', action='store_true',
help='No JSON pretty-printing, dump as a single line')
@cmd2.with_argparser(read_rec_dec_parser)
def do_read_record_decoded(self, opts):
"""Read + decode a record from a record-oriented EF"""
- (data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
+ (data, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
self._cmd.poutput_json(data, opts.oneline)
read_recs_parser = argparse.ArgumentParser()
@cmd2.with_argparser(read_recs_parser)
- def do_read_records(self, opts):
+ def do_read_records(self, _opts):
"""Read all records from a record-oriented EF"""
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
for recnr in range(1, 1 + num_of_rec):
- (data, sw) = self._cmd.lchan.read_record(recnr)
- if (len(data) > 0):
+ (data, _sw) = self._cmd.lchan.read_record(recnr)
+ if len(data) > 0:
recstr = str(data)
else:
recstr = "(empty)"
@@ -864,28 +850,26 @@ class LinFixedEF(CardEF):
# collect all results in list so they are rendered as JSON list when printing
data_list = []
for recnr in range(1, 1 + num_of_rec):
- (data, sw) = self._cmd.lchan.read_record_dec(recnr)
+ (data, _sw) = self._cmd.lchan.read_record_dec(recnr)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
upd_rec_parser = argparse.ArgumentParser()
upd_rec_parser.add_argument(
- 'record_nr', type=int, help='Number of record to be read')
- upd_rec_parser.add_argument(
- 'data', help='Data bytes (hex format) to write')
+ 'record_nr', type=auto_uint8, help='Number of record to be read')
+ upd_rec_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
@cmd2.with_argparser(upd_rec_parser)
def do_update_record(self, opts):
"""Update (write) data to a record-oriented EF"""
- (data, sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
+ (data, _sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
if data:
self._cmd.poutput(data)
upd_rec_dec_parser = argparse.ArgumentParser()
upd_rec_dec_parser.add_argument(
- 'record_nr', type=int, help='Number of record to be read')
- upd_rec_dec_parser.add_argument(
- 'data', help='Abstract data (JSON format) to write')
+ 'record_nr', type=auto_uint8, help='Number of record to be read')
+ upd_rec_dec_parser.add_argument('data', help='Abstract data (JSON format) to write')
upd_rec_dec_parser.add_argument('--json-path', type=str,
help='JSON path to modify specific element of record only')
@@ -893,24 +877,24 @@ class LinFixedEF(CardEF):
def do_update_record_decoded(self, opts):
"""Encode + Update (write) data to a record-oriented EF"""
if opts.json_path:
- (data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
+ (data_json, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
js_path_modify(data_json, opts.json_path,
json.loads(opts.data))
else:
data_json = json.loads(opts.data)
- (data, sw) = self._cmd.lchan.update_record_dec(
+ (data, _sw) = self._cmd.lchan.update_record_dec(
opts.record_nr, data_json)
if data:
self._cmd.poutput(data)
edit_rec_dec_parser = argparse.ArgumentParser()
edit_rec_dec_parser.add_argument(
- 'record_nr', type=int, help='Number of record to be edited')
+ 'record_nr', type=auto_uint8, help='Number of record to be edited')
@cmd2.with_argparser(edit_rec_dec_parser)
def do_edit_record_decoded(self, opts):
"""Edit the JSON representation of one record in an editor."""
- (orig_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
+ (orig_json, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
filename = '%s/file' % dirname
# write existing data as JSON to file
@@ -923,13 +907,13 @@ class LinFixedEF(CardEF):
if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write")
else:
- (data, sw) = self._cmd.lchan.update_record_dec(
+ (data, _sw) = self._cmd.lchan.update_record_dec(
opts.record_nr, edited_json)
if data:
self._cmd.poutput_json(data)
def __init__(self, fid: str, sfid: str = None, name: str = None, desc: str = None,
- parent: Optional[CardDF] = None, rec_len: Size = (1, None), **kwargs):
+ parent: Optional[CardDF] = None, rec_len: Size = (1, None), leftpad: bool = False, **kwargs):
"""
Args:
fid : File Identifier (4 hex digits)
@@ -938,9 +922,11 @@ class LinFixedEF(CardEF):
desc : Description of the file
parent : Parent CardFile object within filesystem hierarchy
rec_len : Tuple of (minimum_length, recommended_length)
+ leftpad: On write, data must be padded from the left to fit pysical record length
"""
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, **kwargs)
self.rec_len = rec_len
+ self.leftpad = leftpad
self.shell_commands = [self.ShellCommands()]
self._construct = None
self._tlv = None
@@ -967,7 +953,7 @@ class LinFixedEF(CardEF):
return method(raw_bin_data, record_nr=record_nr)
if self._construct:
return parse_construct(self._construct, raw_bin_data)
- elif self._tlv:
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_tlv(raw_bin_data)
return t.to_dict()
@@ -995,7 +981,7 @@ class LinFixedEF(CardEF):
return method(raw_hex_data, record_nr=record_nr)
if self._construct:
return parse_construct(self._construct, raw_bin_data)
- elif self._tlv:
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_tlv(raw_bin_data)
return t.to_dict()
@@ -1022,8 +1008,8 @@ class LinFixedEF(CardEF):
raw_bin_data = method(abstract_data, record_nr=record_nr)
return b2h(raw_bin_data)
if self._construct:
- return b2h(self._construct.build(abstract_data))
- elif self._tlv:
+ return b2h(build_construct(self._construct, abstract_data))
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return b2h(t.to_tlv())
@@ -1050,8 +1036,8 @@ class LinFixedEF(CardEF):
if callable(method):
return h2b(method(abstract_data, record_nr=record_nr))
if self._construct:
- return self._construct.build(abstract_data)
- elif self._tlv:
+ return build_construct(self._construct, abstract_data)
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return t.to_tlv()
@@ -1114,7 +1100,7 @@ class TransRecEF(TransparentEF):
return method(raw_bin_data)
if self._construct:
return parse_construct(self._construct, raw_bin_data)
- elif self._tlv:
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_tlv(raw_bin_data)
return t.to_dict()
@@ -1141,7 +1127,7 @@ class TransRecEF(TransparentEF):
return method(raw_hex_data)
if self._construct:
return parse_construct(self._construct, raw_bin_data)
- elif self._tlv:
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_tlv(raw_bin_data)
return t.to_dict()
@@ -1166,8 +1152,8 @@ class TransRecEF(TransparentEF):
if callable(method):
return b2h(method(abstract_data))
if self._construct:
- return b2h(filter_dict(self._construct.build(abstract_data)))
- elif self._tlv:
+ return b2h(filter_dict(build_construct(self._construct, abstract_data)))
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return b2h(t.to_tlv())
@@ -1193,8 +1179,8 @@ class TransRecEF(TransparentEF):
if callable(method):
return h2b(method(abstract_data))
if self._construct:
- return filter_dict(self._construct.build(abstract_data))
- elif self._tlv:
+ return filter_dict(build_construct(self._construct, abstract_data))
+ if self._tlv:
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
t.from_dict(abstract_data)
return t.to_tlv()
@@ -1224,9 +1210,6 @@ class BerTlvEF(CardEF):
class ShellCommands(CommandSet):
"""Shell commands specific for BER-TLV EFs."""
- def __init__(self):
- super().__init__()
-
retrieve_data_parser = argparse.ArgumentParser()
retrieve_data_parser.add_argument(
'tag', type=auto_int, help='BER-TLV Tag of value to retrieve')
@@ -1234,10 +1217,10 @@ class BerTlvEF(CardEF):
@cmd2.with_argparser(retrieve_data_parser)
def do_retrieve_data(self, opts):
"""Retrieve (Read) data from a BER-TLV EF"""
- (data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
+ (data, _sw) = self._cmd.lchan.retrieve_data(opts.tag)
self._cmd.poutput(data)
- def do_retrieve_tags(self, opts):
+ def do_retrieve_tags(self, _opts):
"""List tags available in a given BER-TLV EF"""
tags = self._cmd.lchan.retrieve_tags()
self._cmd.poutput(tags)
@@ -1245,13 +1228,12 @@ class BerTlvEF(CardEF):
set_data_parser = argparse.ArgumentParser()
set_data_parser.add_argument(
'tag', type=auto_int, help='BER-TLV Tag of value to set')
- set_data_parser.add_argument(
- 'data', help='Data bytes (hex format) to write')
+ set_data_parser.add_argument('data', type=is_hexstr, help='Data bytes (hex format) to write')
@cmd2.with_argparser(set_data_parser)
def do_set_data(self, opts):
"""Set (Write) data for a given tag in a BER-TLV EF"""
- (data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
+ (data, _sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
if data:
self._cmd.poutput(data)
@@ -1262,7 +1244,7 @@ class BerTlvEF(CardEF):
@cmd2.with_argparser(del_data_parser)
def do_delete_data(self, opts):
"""Delete data for a given tag in a BER-TLV EF"""
- (data, sw) = self._cmd.lchan.set_data(opts.tag, None)
+ (data, _sw) = self._cmd.lchan.set_data(opts.tag, None)
if data:
self._cmd.poutput(data)
@@ -1282,484 +1264,6 @@ class BerTlvEF(CardEF):
self.size = size
self.shell_commands = [self.ShellCommands()]
-
-class RuntimeState:
- """Represent the runtime state of a session with a card."""
-
- def __init__(self, card, profile: 'CardProfile'):
- """
- Args:
- card : pysim.cards.Card instance
- profile : CardProfile instance
- """
- self.mf = CardMF(profile=profile)
- self.card = card
- self.profile = profile
- self.lchan = {}
- # the basic logical channel always exists
- self.lchan[0] = RuntimeLchan(0, self)
-
- # make sure the class and selection control bytes, which are specified
- # by the card profile are used
- self.card.set_apdu_parameter(
- cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
-
- # add application ADFs + MF-files from profile
- apps = self._match_applications()
- for a in apps:
- if a.adf:
- self.mf.add_application_df(a.adf)
- for f in self.profile.files_in_mf:
- self.mf.add_file(f)
- self.conserve_write = True
-
- # make sure that when the runtime state is created, the card is also
- # in a defined state.
- self.reset()
-
- def _match_applications(self):
- """match the applications from the profile with applications on the card"""
- apps_profile = self.profile.applications
-
- # When the profile does not feature any applications, then we are done already
- if not apps_profile:
- return []
-
- # Read AIDs from card and match them against the applications defined by the
- # card profile
- aids_card = self.card.read_aids()
- apps_taken = []
- if aids_card:
- aids_taken = []
- print("AIDs on card:")
- for a in aids_card:
- for f in apps_profile:
- if f.aid in a:
- print(" %s: %s (EF.DIR)" % (f.name, a))
- aids_taken.append(a)
- apps_taken.append(f)
- aids_unknown = set(aids_card) - set(aids_taken)
- for a in aids_unknown:
- print(" unknown: %s (EF.DIR)" % a)
- else:
- print("warning: EF.DIR seems to be empty!")
-
- # Some card applications may not be registered in EF.DIR, we will actively
- # probe for those applications
- for f in set(apps_profile) - set(apps_taken):
- try:
- data, sw = self.card.select_adf_by_aid(f.aid)
- if sw == "9000":
- print(" %s: %s" % (f.name, f.aid))
- apps_taken.append(f)
- except (SwMatchError, ProtocolError):
- pass
- return apps_taken
-
- def reset(self, cmd_app=None) -> Hexstr:
- """Perform physical card reset and obtain ATR.
- Args:
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # delete all lchan != 0 (basic lchan)
- for lchan_nr in self.lchan.keys():
- if lchan_nr == 0:
- continue
- del self.lchan[lchan_nr]
- atr = i2h(self.card.reset())
- # select MF to reset internal state and to verify card really works
- self.lchan[0].select('MF', cmd_app)
- self.lchan[0].selected_adf = None
- return atr
-
- def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
- """Add a logical channel to the runtime state. You shouldn't call this
- directly but always go through RuntimeLchan.add_lchan()."""
- if lchan_nr in self.lchan.keys():
- raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
- self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
- return self.lchan[lchan_nr]
-
- def del_lchan(self, lchan_nr: int):
- if lchan_nr in self.lchan.keys():
- del self.lchan[lchan_nr]
- return True
- else:
- return False
-
- def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
- lchan_nr = lchan_nr_from_cla(cla)
- if lchan_nr in self.lchan.keys():
- return self.lchan[lchan_nr]
- else:
- return None
-
-
-class RuntimeLchan:
- """Represent the runtime state of a logical channel with a card."""
-
- def __init__(self, lchan_nr: int, rs: RuntimeState):
- self.lchan_nr = lchan_nr
- self.rs = rs
- self.selected_file = self.rs.mf
- self.selected_adf = None
- self.selected_file_fcp = None
- self.selected_file_fcp_hex = None
-
- def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
- """Add a new logical channel from the current logical channel. Just affects
- internal state, doesn't actually open a channel with the UICC."""
- new_lchan = self.rs.add_lchan(lchan_nr)
- # See TS 102 221 Table 8.3
- if self.lchan_nr != 0:
- new_lchan.selected_file = self.get_cwd()
- new_lchan.selected_adf = self.selected_adf
- return new_lchan
-
- def selected_file_descriptor_byte(self) -> dict:
- return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
-
- def selected_file_shareable(self) -> bool:
- return self.selected_file_descriptor_byte()['shareable']
-
- def selected_file_structure(self) -> str:
- return self.selected_file_descriptor_byte()['structure']
-
- def selected_file_type(self) -> str:
- return self.selected_file_descriptor_byte()['file_type']
-
- def selected_file_num_of_rec(self) -> Optional[int]:
- return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
-
- def get_cwd(self) -> CardDF:
- """Obtain the current working directory.
-
- Returns:
- CardDF instance
- """
- if isinstance(self.selected_file, CardDF):
- return self.selected_file
- else:
- return self.selected_file.parent
-
- def get_application_df(self) -> Optional[CardADF]:
- """Obtain the currently selected application DF (if any).
-
- Returns:
- CardADF() instance or None"""
- # iterate upwards from selected file; check if any is an ADF
- node = self.selected_file
- while node.parent != node:
- if isinstance(node, CardADF):
- return node
- node = node.parent
- return None
-
- def interpret_sw(self, sw: str):
- """Interpret a given status word relative to the currently selected application
- or the underlying card profile.
-
- Args:
- sw : Status word as string of 4 hex digits
-
- Returns:
- Tuple of two strings
- """
- res = None
- adf = self.get_application_df()
- if adf:
- app = adf.application
- # The application either comes with its own interpret_sw
- # method or we will use the interpret_sw method from the
- # card profile.
- if app and hasattr(app, "interpret_sw"):
- res = app.interpret_sw(sw)
- return res or self.rs.profile.interpret_sw(sw)
-
- def probe_file(self, fid: str, cmd_app=None):
- """Blindly try to select a file and automatically add a matching file
- object if the file actually exists."""
- if not is_hex(fid, 4, 4):
- raise ValueError(
- "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
-
- try:
- (data, sw) = self.rs.card._scc.select_file(fid)
- except SwMatchError as swm:
- k = self.interpret_sw(swm.sw_actual)
- if not k:
- raise(swm)
- raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
-
- select_resp = self.selected_file.decode_select_response(data)
- if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
- f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
- desc="dedicated file, manually added at runtime")
- else:
- if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
- f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
- desc="elementary file, manually added at runtime")
- else:
- f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
- desc="elementary file, manually added at runtime")
-
- self.selected_file.add_files([f])
- self.selected_file = f
- return select_resp, data
-
- def _select_pre(self, cmd_app):
- # unregister commands of old file
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.unregister_command_set(c)
-
- def _select_post(self, cmd_app):
- # register commands of new file
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.register_command_set(c)
-
- def select_file(self, file: CardFile, cmd_app=None):
- """Select a file (EF, DF, ADF, MF, ...).
-
- Args:
- file : CardFile [or derived class] instance
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # we need to find a path from our self.selected_file to the destination
- inter_path = self.selected_file.build_select_path_to(file)
- if not inter_path:
- raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
-
- self._select_pre(cmd_app)
-
- for p in inter_path:
- try:
- if isinstance(p, CardADF):
- (data, sw) = self.rs.card.select_adf_by_aid(p.aid)
- self.selected_adf = p
- else:
- (data, sw) = self.rs.card._scc.select_file(p.fid)
- self.selected_file = p
- except SwMatchError as swm:
- self._select_post(cmd_app)
- raise(swm)
-
- self._select_post(cmd_app)
-
- def select(self, name: str, cmd_app=None):
- """Select a file (EF, DF, ADF, MF, ...).
-
- Args:
- name : Name of file to select
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # handling of entire paths with multiple directories/elements
- if '/' in name:
- prev_sel_file = self.selected_file
- pathlist = name.split('/')
- # treat /DF.GSM/foo like MF/DF.GSM/foo
- if pathlist[0] == '':
- pathlist[0] = 'MF'
- try:
- for p in pathlist:
- self.select(p, cmd_app)
- return
- except Exception as e:
- # if any intermediate step fails, go back to where we were
- self.select_file(prev_sel_file, cmd_app)
- raise e
-
- sels = self.selected_file.get_selectables()
- if is_hex(name):
- name = name.lower()
-
- self._select_pre(cmd_app)
-
- if name in sels:
- f = sels[name]
- try:
- if isinstance(f, CardADF):
- (data, sw) = self.rs.card.select_adf_by_aid(f.aid)
- else:
- (data, sw) = self.rs.card._scc.select_file(f.fid)
- self.selected_file = f
- except SwMatchError as swm:
- k = self.interpret_sw(swm.sw_actual)
- if not k:
- raise(swm)
- raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
- select_resp = f.decode_select_response(data)
- else:
- (select_resp, data) = self.probe_file(name, cmd_app)
-
- # store the raw + decoded FCP for later reference
- self.selected_file_fcp_hex = data
- self.selected_file_fcp = select_resp
-
- self._select_post(cmd_app)
- return select_resp
-
- def status(self):
- """Request STATUS (current selected file FCP) from card."""
- (data, sw) = self.rs.card._scc.status()
- return self.selected_file.decode_select_response(data)
-
- def get_file_for_selectable(self, name: str):
- sels = self.selected_file.get_selectables()
- return sels[name]
-
- def activate_file(self, name: str):
- """Request ACTIVATE FILE of specified file."""
- sels = self.selected_file.get_selectables()
- f = sels[name]
- data, sw = self.rs.card._scc.activate_file(f.fid)
- return data, sw
-
- def read_binary(self, length: int = None, offset: int = 0):
- """Read [part of] a transparent EF binary data.
-
- Args:
- length : Amount of data to read (None: as much as possible)
- offset : Offset into the file from which to read 'length' bytes
- Returns:
- binary data read from the file
- """
- if not isinstance(self.selected_file, TransparentEF):
- raise TypeError("Only works with TransparentEF")
- return self.rs.card._scc.read_binary(self.selected_file.fid, length, offset)
-
- def read_binary_dec(self) -> Tuple[dict, str]:
- """Read [part of] a transparent EF binary data and decode it.
-
- Args:
- length : Amount of data to read (None: as much as possible)
- offset : Offset into the file from which to read 'length' bytes
- Returns:
- abstract decode data read from the file
- """
- (data, sw) = self.read_binary()
- dec_data = self.selected_file.decode_hex(data)
- return (dec_data, sw)
-
- def update_binary(self, data_hex: str, offset: int = 0):
- """Update transparent EF binary data.
-
- Args:
- data_hex : hex string of data to be written
- offset : Offset into the file from which to write 'data_hex'
- """
- if not isinstance(self.selected_file, TransparentEF):
- raise TypeError("Only works with TransparentEF")
- return self.rs.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
-
- def update_binary_dec(self, data: dict):
- """Update transparent EF from abstract data. Encodes the data to binary and
- then updates the EF with it.
-
- Args:
- data : abstract data which is to be encoded and written
- """
- data_hex = self.selected_file.encode_hex(data)
- return self.update_binary(data_hex)
-
- def read_record(self, rec_nr: int = 0):
- """Read a record as binary data.
-
- Args:
- rec_nr : Record number to read
- Returns:
- hex string of binary data contained in record
- """
- if not isinstance(self.selected_file, LinFixedEF):
- raise TypeError("Only works with Linear Fixed EF")
- # returns a string of hex nibbles
- return self.rs.card._scc.read_record(self.selected_file.fid, rec_nr)
-
- def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
- """Read a record and decode it to abstract data.
-
- Args:
- rec_nr : Record number to read
- Returns:
- abstract data contained in record
- """
- (data, sw) = self.read_record(rec_nr)
- return (self.selected_file.decode_record_hex(data, rec_nr), sw)
-
- def update_record(self, rec_nr: int, data_hex: str):
- """Update a record with given binary data
-
- Args:
- rec_nr : Record number to read
- data_hex : Hex string binary data to be written
- """
- if not isinstance(self.selected_file, LinFixedEF):
- raise TypeError("Only works with Linear Fixed EF")
- return self.rs.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.rs.conserve_write)
-
- def update_record_dec(self, rec_nr: int, data: dict):
- """Update a record with given abstract data. Will encode abstract to binary data
- and then write it to the given record on the card.
-
- Args:
- rec_nr : Record number to read
- data_hex : Abstract data to be written
- """
- data_hex = self.selected_file.encode_record_hex(data, rec_nr)
- return self.update_record(rec_nr, data_hex)
-
- def retrieve_data(self, tag: int = 0):
- """Read a DO/TLV as binary data.
-
- Args:
- tag : Tag of TLV/DO to read
- Returns:
- hex string of full BER-TLV DO including Tag and Length
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- # returns a string of hex nibbles
- return self.rs.card._scc.retrieve_data(self.selected_file.fid, tag)
-
- def retrieve_tags(self):
- """Retrieve tags available on BER-TLV EF.
-
- Returns:
- list of integer tags contained in EF
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- data, sw = self.rs.card._scc.retrieve_data(self.selected_file.fid, 0x5c)
- tag, length, value, remainder = bertlv_parse_one(h2b(data))
- return list(value)
-
- def set_data(self, tag: int, data_hex: str):
- """Update a TLV/DO with given binary data
-
- Args:
- tag : Tag of TLV/DO to be written
- data_hex : Hex string binary data to be written (value portion)
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- return self.rs.card._scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
-
- def unregister_cmds(self, cmd_app=None):
- """Unregister all file specific commands."""
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.unregister_command_set(c)
-
-
-class FileData:
- """Represent the runtime, on-card data."""
-
- def __init__(self, fdesc):
- self.desc = fdesc
- self.fcp = None
-
-
def interpret_sw(sw_data: dict, sw: str):
"""Interpret a given status word.
@@ -1790,9 +1294,11 @@ class CardApplication:
adf : ADF name
sw : Dict of status word conversions
"""
+ if aid:
+ aid = aid.lower()
self.name = name
self.adf = adf
- self.sw = sw or dict()
+ self.sw = sw or {}
# back-reference from ADF to Applicaiton
if self.adf:
self.aid = aid or self.adf.aid
@@ -1814,6 +1320,10 @@ class CardApplication:
"""
return interpret_sw(self.sw, sw)
+ def export(self, cmd, context, **kwargs):
+ """Method called to export the state of the application during pySim-shell 'export' command."""
+ cmd.poutput("# application: %s" % (self.name))
+
class CardModel(abc.ABC):
"""A specific card model, typically having some additional vendor-specific files. All
@@ -1823,7 +1333,7 @@ class CardModel(abc.ABC):
@classmethod
@abc.abstractmethod
- def add_files(cls, rs: RuntimeState):
+ def add_files(cls, rs: 'RuntimeState'):
"""Add model specific files to given RuntimeState."""
@classmethod
@@ -1838,7 +1348,7 @@ class CardModel(abc.ABC):
return False
@staticmethod
- def apply_matching_models(scc: SimCardCommands, rs: RuntimeState):
+ def apply_matching_models(scc: SimCardCommands, rs: 'RuntimeState'):
"""Check if any of the CardModel sub-classes 'match' the currently inserted card
(by ATR or overriding the 'match' method). If so, call their 'add_files'
method."""
diff --git a/pySim/global_platform.py b/pySim/global_platform.py
deleted file mode 100644
index 1c31ddc..0000000
--- a/pySim/global_platform.py
+++ /dev/null
@@ -1,256 +0,0 @@
-# coding=utf-8
-"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
-
-(C) 2022 by Harald Welte <laforge@osmocom.org>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-
-from typing import Optional, List, Dict, Tuple
-from construct import Optional as COptional
-from construct import *
-from bidict import bidict
-from pySim.construct import *
-from pySim.utils import *
-from pySim.filesystem import *
-from pySim.tlv import *
-from pySim.profile import CardProfile
-
-sw_table = {
- 'Warnings': {
- '6200': 'Logical Channel already closed',
- '6283': 'Card Life Cycle State is CARD_LOCKED',
- '6310': 'More data available',
- },
- 'Execution errors': {
- '6400': 'No specific diagnosis',
- '6581': 'Memory failure',
- },
- 'Checking errors': {
- '6700': 'Wrong length in Lc',
- },
- 'Functions in CLA not supported': {
- '6881': 'Logical channel not supported or active',
- '6882': 'Secure messaging not supported',
- },
- 'Command not allowed': {
- '6982': 'Security Status not satisfied',
- '6985': 'Conditions of use not satisfied',
- },
- 'Wrong parameters': {
- '6a80': 'Incorrect values in command data',
- '6a81': 'Function not supported e.g. card Life Cycle State is CARD_LOCKED',
- '6a82': 'Application not found',
- '6a84': 'Not enough memory space',
- '6a86': 'Incorrect P1 P2',
- '6a88': 'Referenced data not found',
- },
- 'GlobalPlatform': {
- '6d00': 'Invalid instruction',
- '6e00': 'Invalid class',
- },
- 'Application errors': {
- '9484': 'Algorithm not supported',
- '9485': 'Invalid key check value',
- },
-}
-
-# GlobalPlatform 2.1.1 Section 9.1.6
-KeyType = Enum(Byte, des=0x80,
- rsa_public_exponent_e_cleartex=0xA0,
- rsa_modulus_n_cleartext=0xA1,
- rsa_modulus_n=0xA2,
- rsa_private_exponent_d=0xA3,
- rsa_chines_remainder_p=0xA4,
- rsa_chines_remainder_q=0xA5,
- rsa_chines_remainder_pq=0xA6,
- rsa_chines_remainder_dpi=0xA7,
- rsa_chines_remainder_dqi=0xA8,
- not_available=0xff)
-
-# GlobalPlatform 2.1.1 Section 9.3.3.1
-# example:
-# e0 48
-# c0 04 01708010
-# c0 04 02708010
-# c0 04 03708010
-# c0 04 01018010
-# c0 04 02018010
-# c0 04 03018010
-# c0 04 01028010
-# c0 04 02028010
-# c0 04 03028010
-# c0 04 01038010
-# c0 04 02038010
-# c0 04 03038010
-class KeyInformationData(BER_TLV_IE, tag=0xc0):
- _construct = Struct('key_identifier'/Byte, 'key_version_number'/Byte,
- 'key_types'/GreedyRange(KeyType))
-class KeyInformation(BER_TLV_IE, tag=0xe0, nested=[KeyInformationData]):
- pass
-
-# card data sample, returned in response to GET DATA (80ca006600):
-# 66 31
-# 73 2f
-# 06 07
-# 2a864886fc6b01
-# 60 0c
-# 06 0a
-# 2a864886fc6b02020101
-# 63 09
-# 06 07
-# 2a864886fc6b03
-# 64 0b
-# 06 09
-# 2a864886fc6b040215
-
-# GlobalPlatform 2.1.1 Table F-1
-class ObjectIdentifier(BER_TLV_IE, tag=0x06):
- _construct = GreedyBytes
-class CardManagementTypeAndVersion(BER_TLV_IE, tag=0x60, nested=[ObjectIdentifier]):
- pass
-class CardIdentificationScheme(BER_TLV_IE, tag=0x63, nested=[ObjectIdentifier]):
- pass
-class SecureChannelProtocolOfISD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
- pass
-class CardConfigurationDetails(BER_TLV_IE, tag=0x65):
- _construct = GreedyBytes
-class CardChipDetails(BER_TLV_IE, tag=0x66):
- _construct = GreedyBytes
-class CardRecognitionData(BER_TLV_IE, tag=0x73, nested=[ObjectIdentifier,
- CardManagementTypeAndVersion,
- CardIdentificationScheme,
- SecureChannelProtocolOfISD,
- CardConfigurationDetails,
- CardChipDetails]):
- pass
-class CardData(BER_TLV_IE, tag=0x66, nested=[CardRecognitionData]):
- pass
-
-# GlobalPlatform 2.1.1 Table F-2
-class SecureChannelProtocolOfSelectedSD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
- pass
-class SecurityDomainMgmtData(BER_TLV_IE, tag=0x73, nested=[CardManagementTypeAndVersion,
- CardIdentificationScheme,
- SecureChannelProtocolOfSelectedSD,
- CardConfigurationDetails,
- CardChipDetails]):
- pass
-
-# GlobalPlatform 2.1.1 Section 9.1.1
-IsdLifeCycleState = Enum(Byte, op_ready=0x01, initialized=0x07, secured=0x0f,
- card_locked = 0x7f, terminated=0xff)
-
-# GlobalPlatform 2.1.1 Section 9.9.3.1
-class ApplicationID(BER_TLV_IE, tag=0x84):
- _construct = GreedyBytes
-
-# GlobalPlatform 2.1.1 Section 9.9.3.1
-class SecurityDomainManagementData(BER_TLV_IE, tag=0x73):
- _construct = GreedyBytes
-
-# GlobalPlatform 2.1.1 Section 9.9.3.1
-class ApplicationProductionLifeCycleData(BER_TLV_IE, tag=0x9f6e):
- _construct = GreedyBytes
-
-# GlobalPlatform 2.1.1 Section 9.9.3.1
-class MaximumLengthOfDataFieldInCommandMessage(BER_TLV_IE, tag=0x9f65):
- _construct = GreedyInteger()
-
-# GlobalPlatform 2.1.1 Section 9.9.3.1
-class ProprietaryData(BER_TLV_IE, tag=0xA5, nested=[SecurityDomainManagementData,
- ApplicationProductionLifeCycleData,
- MaximumLengthOfDataFieldInCommandMessage]):
- pass
-
-# GlobalPlatform 2.1.1 Section 9.9.3.1
-class FciTemplate(BER_TLV_IE, tag=0x6f, nested=[ApplicationID, SecurityDomainManagementData,
- ApplicationProductionLifeCycleData,
- MaximumLengthOfDataFieldInCommandMessage,
- ProprietaryData]):
- pass
-
-class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
- _construct = BcdAdapter(GreedyBytes)
-
-class CardImageNumber(BER_TLV_IE, tag=0x45):
- _construct = BcdAdapter(GreedyBytes)
-
-class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
- _construct = GreedyInteger()
-
-class ConfirmationCounter(BER_TLV_IE, tag=0xc2):
- _construct = GreedyInteger()
-
-# Collection of all the data objects we can get from GET DATA
-class DataCollection(TLV_IE_Collection, nested=[IssuerIdentificationNumber,
- CardImageNumber,
- CardData,
- KeyInformation,
- SequenceCounterOfDefaultKvn,
- ConfirmationCounter]):
- pass
-
-def decode_select_response(resp_hex: str) -> object:
- t = FciTemplate()
- t.from_tlv(h2b(resp_hex))
- d = t.to_dict()
- return flatten_dict_lists(d['fci_template'])
-
-# Application Dedicated File of a Security Domain
-class ADF_SD(CardADF):
- def __init__(self, aid: str, name: str, desc: str):
- super().__init__(aid=aid, fid=None, sfid=None, name=name, desc=desc)
- self.shell_commands += [self.AddlShellCommands()]
-
- @staticmethod
- def decode_select_response(res_hex: str) -> object:
- return decode_select_response(res_hex)
-
- @with_default_category('Application-Specific Commands')
- class AddlShellCommands(CommandSet):
- def __init__(self):
- super().__init__()
-
- def do_get_data(self, opts):
- tlv_cls_name = opts.arg_list[0]
- tlv_cls = DataCollection().members_by_name[tlv_cls_name]
- (data, sw) = self._cmd.card._scc.get_data(cla=0x80, tag=tlv_cls.tag)
- ie = tlv_cls()
- ie.from_tlv(h2b(data))
- self._cmd.poutput_json(ie.to_dict())
-
- def complete_get_data(self, text, line, begidx, endidx) -> List[str]:
- #data_dict = {camel_to_snake(str(x.__name__)): x for x in DataCollection.possible_nested}
- data_dict = {str(x.__name__): x for x in DataCollection.possible_nested}
- index_dict = {1: data_dict}
- return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
-
-# Card Application of a Security Domain
-class CardApplicationSD(CardApplication):
- def __init__(self, aid: str, name: str, desc: str):
- super().__init__(name, adf=ADF_SD(aid, name, desc), sw=sw_table)
-
-# Card Application of Issuer Security Domain
-class CardApplicationISD(CardApplicationSD):
- # FIXME: ISD AID is not static, but could be different. One can select the empty
- # application using '00a4040000' and then parse the response FCI to get the ISD AID
- def __init__(self, aid='a000000003000000'):
- super().__init__(aid=aid, name='ADF.ISD', desc='Issuer Security Domain')
-
-#class CardProfileGlobalPlatform(CardProfile):
-# ORDER = 23
-#
-# def __init__(self, name='GlobalPlatform'):
-# super().__init__(name, desc='GlobalPlatfomr 2.1.1', cla=['00','80','84'], sw=sw_table)
diff --git a/pySim/global_platform/__init__.py b/pySim/global_platform/__init__.py
new file mode 100644
index 0000000..0be316b
--- /dev/null
+++ b/pySim/global_platform/__init__.py
@@ -0,0 +1,877 @@
+# coding=utf-8
+"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
+
+(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from copy import deepcopy
+from typing import Optional, List, Dict, Tuple
+from construct import Optional as COptional
+from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
+from Cryptodome.Random import get_random_bytes
+from Cryptodome.Cipher import DES, DES3, AES
+from pySim.card_key_provider import card_key_provider_get_field
+from pySim.global_platform.scp import SCP02, SCP03
+from pySim.construct import *
+from pySim.utils import *
+from pySim.filesystem import *
+from pySim.tlv import *
+from pySim.profile import CardProfile
+
+sw_table = {
+ 'Warnings': {
+ '6200': 'Logical Channel already closed',
+ '6283': 'Card Life Cycle State is CARD_LOCKED',
+ '6310': 'More data available',
+ },
+ 'Execution errors': {
+ '6400': 'No specific diagnosis',
+ '6581': 'Memory failure',
+ },
+ 'Checking errors': {
+ '6700': 'Wrong length in Lc',
+ },
+ 'Functions in CLA not supported': {
+ '6881': 'Logical channel not supported or active',
+ '6882': 'Secure messaging not supported',
+ },
+ 'Command not allowed': {
+ '6982': 'Security Status not satisfied',
+ '6985': 'Conditions of use not satisfied',
+ },
+ 'Wrong parameters': {
+ '6a80': 'Incorrect values in command data',
+ '6a81': 'Function not supported e.g. card Life Cycle State is CARD_LOCKED',
+ '6a82': 'Application not found',
+ '6a84': 'Not enough memory space',
+ '6a86': 'Incorrect P1 P2',
+ '6a88': 'Referenced data not found',
+ },
+ 'GlobalPlatform': {
+ '6d00': 'Invalid instruction',
+ '6e00': 'Invalid class',
+ },
+ 'Application errors': {
+ '9484': 'Algorithm not supported',
+ '9485': 'Invalid key check value',
+ },
+}
+
+# GlobalPlatform 2.1.1 Section 9.1.6
+KeyType = Enum(Byte, des=0x80,
+ tls_psk=0x85, # v2.3.1 Section 11.1.8
+ aes=0x88, # v2.3.1 Section 11.1.8
+ hmac_sha1=0x90, # v2.3.1 Section 11.1.8
+ hmac_sha1_160=0x91, # v2.3.1 Section 11.1.8
+ rsa_public_exponent_e_cleartex=0xA0,
+ rsa_modulus_n_cleartext=0xA1,
+ rsa_modulus_n=0xA2,
+ rsa_private_exponent_d=0xA3,
+ rsa_chines_remainder_p=0xA4,
+ rsa_chines_remainder_q=0xA5,
+ rsa_chines_remainder_pq=0xA6,
+ rsa_chines_remainder_dpi=0xA7,
+ rsa_chines_remainder_dqi=0xA8,
+ ecc_public_key=0xB0, # v2.3.1 Section 11.1.8
+ ecc_private_key=0xB1, # v2.3.1 Section 11.1.8
+ ecc_field_parameter_p=0xB2, # v2.3.1 Section 11.1.8
+ ecc_field_parameter_a=0xB3, # v2.3.1 Section 11.1.8
+ ecc_field_parameter_b=0xB4, # v2.3.1 Section 11.1.8
+ ecc_field_parameter_g=0xB5, # v2.3.1 Section 11.1.8
+ ecc_field_parameter_n=0xB6, # v2.3.1 Section 11.1.8
+ ecc_field_parameter_k=0xB7, # v2.3.1 Section 11.1.8
+ ecc_key_parameters_reference=0xF0, # v2.3.1 Section 11.1.8
+ not_available=0xff)
+
+# GlobalPlatform 2.3 Section 11.10.2.1 Table 11-86
+SetStatusScope = Enum(Byte, isd=0x80, app_or_ssd=0x40, isd_and_assoc_apps=0xc0)
+
+# GlobalPlatform 2.3 section 11.1.1
+CLifeCycleState = Enum(Byte, loaded=0x01, installed=0x03, selectable=0x07, personalized=0x0f, locked=0x83)
+
+# GlobalPlatform 2.1.1 Section 9.3.3.1
+class KeyInformationData(BER_TLV_IE, tag=0xc0):
+ _test_de_encode = [
+ ( 'c00401708010', {"key_identifier": 1, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00402708010', {"key_identifier": 2, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00403708010', {"key_identifier": 3, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00401018010', {"key_identifier": 1, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00402018010', {"key_identifier": 2, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00403018010', {"key_identifier": 3, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00401028010', {"key_identifier": 1, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00402028010', {"key_identifier": 2, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00403038010', {"key_identifier": 3, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00401038010', {"key_identifier": 1, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00402038010', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
+ ( 'c00402038810', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "aes"} ]} ),
+ ]
+ KeyTypeLen = Struct('type'/KeyType, 'length'/Int8ub)
+ _construct = Struct('key_identifier'/Byte, 'key_version_number'/Byte,
+ 'key_types'/GreedyRange(KeyTypeLen))
+class KeyInformation(BER_TLV_IE, tag=0xe0, nested=[KeyInformationData]):
+ pass
+
+# GP v2.3 11.1.9
+KeyUsageQualifier = FlagsEnum(StripTrailerAdapter(GreedyBytes, 2),
+ verification_encryption=0x8000,
+ computation_decipherment=0x4000,
+ sm_response=0x2000,
+ sm_command=0x1000,
+ confidentiality=0x0800,
+ crypto_checksum=0x0400,
+ digital_signature=0x0200,
+ crypto_authorization=0x0100,
+ key_agreement=0x0080)
+
+# GP v2.3 11.1.10
+KeyAccess = Enum(Byte, sd_and_any_assoc_app=0x00, sd_only=0x01, any_assoc_app_but_not_sd=0x02,
+ not_available=0xff)
+
+class KeyLoading:
+ # Global Platform Specification v2.3 Section 11.11.4.2.2.3 DGIs for the CC Private Key
+
+ class KeyUsageQualifier(BER_TLV_IE, tag=0x95):
+ _construct = KeyUsageQualifier
+
+ class KeyAccess(BER_TLV_IE, tag=0x96):
+ _construct = KeyAccess
+
+ class KeyType(BER_TLV_IE, tag=0x80):
+ _construct = KeyType
+
+ class KeyLength(BER_TLV_IE, tag=0x81):
+ _construct = GreedyInteger()
+
+ class KeyIdentifier(BER_TLV_IE, tag=0x82):
+ _construct = Int8ub
+
+ class KeyVersionNumber(BER_TLV_IE, tag=0x83):
+ _construct = Int8ub
+
+ class KeyParameterReferenceValue(BER_TLV_IE, tag=0x85):
+ _construct = Enum(Byte, secp256r1=0x00, secp384r1=0x01, secp521r1=0x02, brainpoolP256r1=0x03,
+ brainpoolP256t1=0x04, brainpoolP384r1=0x05, brainpoolP384t1=0x06,
+ brainpoolP512r1=0x07, brainpoolP512t1=0x08)
+
+ # pylint: disable=undefined-variable
+ class ControlReferenceTemplate(BER_TLV_IE, tag=0xb9,
+ nested=[KeyUsageQualifier,
+ KeyAccess,
+ KeyType,
+ KeyLength,
+ KeyIdentifier,
+ KeyVersionNumber,
+ KeyParameterReferenceValue]):
+ pass
+
+ # Table 11-103
+ class EccPublicKey(DGI_TLV_IE, tag=0x0036):
+ _construct = GreedyBytes
+
+ # Table 11-105
+ class EccPrivateKey(DGI_TLV_IE, tag=0x8137):
+ _construct = GreedyBytes
+
+ # Global Platform Specification v2.3 Section 11.11.4 / Table 11-91
+ class KeyControlReferenceTemplate(DGI_TLV_IE, tag=0x00b9, nested=[ControlReferenceTemplate]):
+ pass
+
+
+# GlobalPlatform v2.3.1 Section H.4 / Table H-6
+class ScpType(BER_TLV_IE, tag=0x80):
+ _construct = HexAdapter(Byte)
+class ListOfSupportedOptions(BER_TLV_IE, tag=0x81):
+ _construct = GreedyBytes
+class SupportedKeysForScp03(BER_TLV_IE, tag=0x82):
+ _construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04)
+class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83):
+ _consuruct = GreedyRange(Int16ub)
+class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03,
+ SupportedTlsCipherSuitesForScp81]):
+ pass
+class PrivilegesAvailableSSD(BER_TLV_IE, tag=0x81):
+ pass
+class PrivilegesAvailableApplication(BER_TLV_IE, tag=0x82):
+ pass
+class SupportedLFDBHAlgorithms(BER_TLV_IE, tag=0x83):
+ pass
+# GlobalPlatform Card Specification v2.3 / Table H-8
+class CiphersForLFDBEncryption(BER_TLV_IE, tag=0x84):
+ _construct = Enum(Byte, tripledes16=0x01, aes128=0x02, aes192=0x04, aes256=0x08,
+ icv_supported_for_lfdb=0x80)
+CipherSuitesForSignatures = FlagsEnum(StripTrailerAdapter(GreedyBytes, 2),
+ rsa1024_pkcsv15_sha1=0x0100,
+ rsa_gt1024_pss_sha256=0x0200,
+ single_des_plus_final_triple_des_mac_16b=0x0400,
+ cmac_aes128=0x0800, cmac_aes192=0x1000, cmac_aes256=0x2000,
+ ecdsa_ecc256_sha256=0x4000, ecdsa_ecc384_sha384=0x8000,
+ ecdsa_ecc512_sha512=0x0001, ecdsa_ecc_521_sha512=0x0002)
+class CiphersForTokens(BER_TLV_IE, tag=0x85):
+ _construct = CipherSuitesForSignatures
+class CiphersForReceipts(BER_TLV_IE, tag=0x86):
+ _construct = CipherSuitesForSignatures
+class CiphersForDAPs(BER_TLV_IE, tag=0x87):
+ _construct = CipherSuitesForSignatures
+class KeyParameterReferenceList(BER_TLV_IE, tag=0x88, nested=[KeyLoading.KeyParameterReferenceValue]):
+ pass
+class CardCapabilityInformation(BER_TLV_IE, tag=0x67, nested=[ScpInformation, PrivilegesAvailableSSD,
+ PrivilegesAvailableApplication,
+ SupportedLFDBHAlgorithms,
+ CiphersForLFDBEncryption, CiphersForTokens,
+ CiphersForReceipts, CiphersForDAPs,
+ KeyParameterReferenceList]):
+ pass
+
+class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
+ _construct = Int8ub
+
+# GlobalPlatform v2.3.1 Section 11.3.3.1.3
+class ApplicationAID(BER_TLV_IE, tag=0x4f):
+ _construct = HexAdapter(GreedyBytes)
+class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
+ pass
+class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
+ pass
+
+# GlobalPlatform v2.3.1 Section 11.3.3.1.2 + TS 102 226
+class NumberOFInstalledApp(BER_TLV_IE, tag=0x81):
+ _construct = GreedyInteger()
+class FreeNonVolatileMemory(BER_TLV_IE, tag=0x82):
+ _construct = GreedyInteger()
+class FreeVolatileMemory(BER_TLV_IE, tag=0x83):
+ _construct = GreedyInteger()
+class ExtendedCardResourcesInfo(BER_TLV_IE, tag=0xff21, nested=[NumberOFInstalledApp, FreeNonVolatileMemory,
+ FreeVolatileMemory]):
+ pass
+
+# GlobalPlatform v2.3.1 Section 7.4.2.4 + GP SPDM
+class SecurityDomainManagerURL(BER_TLV_IE, tag=0x5f50):
+ pass
+
+
+# card data sample, returned in response to GET DATA (80ca006600):
+# 66 31
+# 73 2f
+# 06 07
+# 2a864886fc6b01
+# 60 0c
+# 06 0a
+# 2a864886fc6b02020101
+# 63 09
+# 06 07
+# 2a864886fc6b03
+# 64 0b
+# 06 09
+# 2a864886fc6b040215
+
+# GlobalPlatform 2.1.1 Table F-1
+class ObjectIdentifier(BER_TLV_IE, tag=0x06):
+ _construct = GreedyBytes
+class CardManagementTypeAndVersion(BER_TLV_IE, tag=0x60, nested=[ObjectIdentifier]):
+ pass
+class CardIdentificationScheme(BER_TLV_IE, tag=0x63, nested=[ObjectIdentifier]):
+ pass
+class SecureChannelProtocolOfISD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
+ pass
+class CardConfigurationDetails(BER_TLV_IE, tag=0x65):
+ _construct = GreedyBytes
+class CardChipDetails(BER_TLV_IE, tag=0x66):
+ _construct = GreedyBytes
+class CardRecognitionData(BER_TLV_IE, tag=0x73, nested=[ObjectIdentifier,
+ CardManagementTypeAndVersion,
+ CardIdentificationScheme,
+ SecureChannelProtocolOfISD,
+ CardConfigurationDetails,
+ CardChipDetails]):
+ pass
+class CardData(BER_TLV_IE, tag=0x66, nested=[CardRecognitionData]):
+ pass
+
+# GlobalPlatform 2.1.1 Table F-2
+class SecureChannelProtocolOfSelectedSD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
+ pass
+class SecurityDomainMgmtData(BER_TLV_IE, tag=0x73, nested=[CardManagementTypeAndVersion,
+ CardIdentificationScheme,
+ SecureChannelProtocolOfSelectedSD,
+ CardConfigurationDetails,
+ CardChipDetails]):
+ pass
+
+# GlobalPlatform 2.1.1 Section 9.1.1
+IsdLifeCycleState = Enum(Byte, op_ready=0x01, initialized=0x07, secured=0x0f,
+ card_locked = 0x7f, terminated=0xff)
+
+# GlobalPlatform 2.1.1 Section 9.9.3.1
+class ApplicationID(BER_TLV_IE, tag=0x84):
+ _construct = GreedyBytes
+
+# GlobalPlatform 2.1.1 Section 9.9.3.1
+class SecurityDomainManagementData(BER_TLV_IE, tag=0x73):
+ _construct = GreedyBytes
+
+# GlobalPlatform 2.1.1 Section 9.9.3.1
+class ApplicationProductionLifeCycleData(BER_TLV_IE, tag=0x9f6e):
+ _construct = GreedyBytes
+
+# GlobalPlatform 2.1.1 Section 9.9.3.1
+class MaximumLengthOfDataFieldInCommandMessage(BER_TLV_IE, tag=0x9f65):
+ _construct = GreedyInteger()
+
+# GlobalPlatform 2.1.1 Section 9.9.3.1
+class ProprietaryData(BER_TLV_IE, tag=0xA5, nested=[SecurityDomainManagementData,
+ ApplicationProductionLifeCycleData,
+ MaximumLengthOfDataFieldInCommandMessage]):
+ pass
+
+# explicitly define this list and give it a name so pySim.euicc can reference it
+FciTemplateNestedList = [ApplicationID, SecurityDomainManagementData,
+ ApplicationProductionLifeCycleData,
+ MaximumLengthOfDataFieldInCommandMessage,
+ ProprietaryData]
+
+# GlobalPlatform 2.1.1 Section 9.9.3.1
+class FciTemplate(BER_TLV_IE, tag=0x6f, nested=FciTemplateNestedList):
+ pass
+
+class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
+ _construct = HexAdapter(GreedyBytes)
+
+class CardImageNumber(BER_TLV_IE, tag=0x45):
+ _construct = HexAdapter(GreedyBytes)
+
+class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
+ _construct = GreedyInteger()
+
+class ConfirmationCounter(BER_TLV_IE, tag=0xc2):
+ _construct = GreedyInteger()
+
+# Collection of all the data objects we can get from GET DATA
+class DataCollection(TLV_IE_Collection, nested=[IssuerIdentificationNumber,
+ CardImageNumber,
+ CardData,
+ KeyInformation,
+ SequenceCounterOfDefaultKvn,
+ ConfirmationCounter,
+ # v2.3.1
+ CardCapabilityInformation,
+ CurrentSecurityLevel,
+ ListOfApplications,
+ ExtendedCardResourcesInfo,
+ SecurityDomainManagerURL]):
+ pass
+
+def decode_select_response(resp_hex: str) -> object:
+ t = FciTemplate()
+ t.from_tlv(h2b(resp_hex))
+ d = t.to_dict()
+ return flatten_dict_lists(d['fci_template'])
+
+# 11.4.2.1
+StatusSubset = Enum(Byte, isd=0x80, applications=0x40, files=0x20, files_and_modules=0x10)
+
+
+# Section 11.4.3.1 Table 11-36
+class LifeCycleState(BER_TLV_IE, tag=0x9f70):
+ _construct = CLifeCycleState
+
+# Section 11.4.3.1 Table 11-36 + Section 11.1.2
+class Privileges(BER_TLV_IE, tag=0xc5):
+ # we only support 3-byte encoding. Can't use StripTrailerAdapter as length==2 is not permitted. sigh.
+ _construct = FlagsEnum(Int24ub,
+ security_domain=0x800000, dap_verification=0x400000,
+ delegated_management=0x200000, card_lock=0x100000, card_terminate=0x080000,
+ card_reset=0x040000, cvm_management=0x020000,
+ mandated_dap_verification=0x010000,
+ trusted_path=0x8000, authorized_management=0x4000,
+ token_management=0x2000, global_delete=0x1000, global_lock=0x0800,
+ global_registry=0x0400, final_application=0x0200, global_service=0x0100,
+ receipt_generation=0x80, ciphered_load_file_data_block=0x40,
+ contactless_activation=0x20, contactless_self_activation=0x10)
+
+# Section 11.4.3.1 Table 11-36 + Section 11.1.7
+class ImplicitSelectionParameter(BER_TLV_IE, tag=0xcf):
+ _construct = BitStruct('contactless_io'/Flag,
+ 'contact_io'/Flag,
+ '_rfu'/Flag,
+ 'logical_channel_number'/BitsInteger(5))
+
+# Section 11.4.3.1 Table 11-36
+class ExecutableLoadFileAID(BER_TLV_IE, tag=0xc4):
+ _construct = HexAdapter(GreedyBytes)
+
+# Section 11.4.3.1 Table 11-36
+class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
+ # Note: the Executable Load File Version Number format and contents are beyond the scope of this
+ # specification. It shall consist of the version information contained in the original Load File: on a
+ # Java Card based card, this version number represents the major and minor version attributes of the
+ # original Load File Data Block.
+ _construct = HexAdapter(GreedyBytes)
+
+# Section 11.4.3.1 Table 11-36
+class ExecutableModuleAID(BER_TLV_IE, tag=0x84):
+ _construct = HexAdapter(GreedyBytes)
+
+# Section 11.4.3.1 Table 11-36
+class AssociatedSecurityDomainAID(BER_TLV_IE, tag=0xcc):
+ _construct = HexAdapter(GreedyBytes)
+
+# Section 11.4.3.1 Table 11-36
+class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCycleState, Privileges,
+ ImplicitSelectionParameter, ExecutableLoadFileAID,
+ ExecutableLoadFileVersionNumber,
+ ExecutableModuleAID, AssociatedSecurityDomainAID]):
+ pass
+
+# Application Dedicated File of a Security Domain
+class ADF_SD(CardADF):
+ StoreData = BitStruct('last_block'/Flag,
+ 'encryption'/Enum(BitsInteger(2), none=0, application_dependent=1, rfu=2, encrypted=3),
+ 'structure'/Enum(BitsInteger(2), none=0, dgi=1, ber_tlv=2, rfu=3),
+ '_pad'/Padding(2),
+ 'response'/Enum(Bit, not_expected=0, may_be_returned=1))
+
+ def __init__(self, aid: str, name: str, desc: str):
+ super().__init__(aid=aid, fid=None, sfid=None, name=name, desc=desc)
+ self.shell_commands += [self.AddlShellCommands()]
+
+ def decode_select_response(self, data_hex: str) -> object:
+ return decode_select_response(data_hex)
+
+ @with_default_category('Application-Specific Commands')
+ class AddlShellCommands(CommandSet):
+ get_data_parser = argparse.ArgumentParser()
+ get_data_parser.add_argument('data_object_name', type=str,
+ help='Name of the data object to be retrieved from the card')
+
+ @cmd2.with_argparser(get_data_parser)
+ def do_get_data(self, opts):
+ """Perform the GlobalPlatform GET DATA command in order to obtain some card-specific data."""
+ tlv_cls_name = opts.data_object_name
+ try:
+ tlv_cls = DataCollection().members_by_name[tlv_cls_name]
+ except KeyError:
+ do_names = [camel_to_snake(str(x.__name__)) for x in DataCollection.possible_nested]
+ self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name,
+ do_names))
+ return
+ (data, _sw) = self._cmd.lchan.scc.get_data(cla=0x80, tag=tlv_cls.tag)
+ ie = tlv_cls()
+ ie.from_tlv(h2b(data))
+ self._cmd.poutput_json(ie.to_dict())
+
+ def complete_get_data(self, text, line, begidx, endidx) -> List[str]:
+ data_dict = {camel_to_snake(str(x.__name__)): x for x in DataCollection.possible_nested}
+ index_dict = {1: data_dict}
+ return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
+
+ store_data_parser = argparse.ArgumentParser()
+ store_data_parser.add_argument('--data-structure', type=str, choices=['none','dgi','ber_tlv','rfu'], default='none')
+ store_data_parser.add_argument('--encryption', type=str, choices=['none','application_dependent', 'rfu', 'encrypted'], default='none')
+ store_data_parser.add_argument('--response', type=str, choices=['not_expected','may_be_returned'], default='not_expected')
+ store_data_parser.add_argument('DATA', type=is_hexstr)
+
+ @cmd2.with_argparser(store_data_parser)
+ def do_store_data(self, opts):
+ """Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
+ See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
+ response_permitted = opts.response == 'may_be_returned'
+ 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:
+ """Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
+ See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
+ max_cmd_len = self._cmd.lchan.scc.max_cmd_len
+ # Table 11-89 of GP Card Specification v2.3
+ remainder = data
+ block_nr = 0
+ response = ''
+ while len(remainder):
+ chunk = remainder[:max_cmd_len]
+ remainder = remainder[max_cmd_len:]
+ p1b = build_construct(ADF_SD.StoreData,
+ {'last_block': len(remainder) == 0, 'encryption': encryption,
+ 'structure': structure, 'response': response_permitted})
+ hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk))
+ data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk))
+ block_nr += 1
+ response += data
+ return data
+
+ 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('--key-version-nr', type=auto_uint8, required=True, help='Key Version Number')
+ put_key_parser.add_argument('--key-id', type=auto_uint7, required=True, help='Key Identifier (base)')
+ put_key_parser.add_argument('--key-type', choices=KeyType.ksymapping.values(), action='append', required=True, help='Key Type')
+ put_key_parser.add_argument('--key-data', type=is_hexstr, action='append', required=True, help='Key Data Block')
+ put_key_parser.add_argument('--key-check', type=is_hexstr, action='append', help='Key Check Value')
+ put_key_parser.add_argument('--suppress-key-check', action='store_true', help='Suppress generation of Key Check Values')
+
+ @cmd2.with_argparser(put_key_parser)
+ def do_put_key(self, opts):
+ """Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
+ See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
+
+ The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
+ otherwise be automatically generated for DES and AES keys. You can suppress the latter using
+ `--suppress-key-check`.
+
+ Example (SCP80 KIC/KID/KIK):
+ put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
+ --key-type aes --key-data 101112131415161718191a1b1c1d1e1f
+ --key-type aes --key-data 202122232425262728292a2b2c2d2e2f
+
+ Example (SCP81 TLS-PSK/KEK):
+ put_key --key-version-nr 0x40 --key-id 0x01 --key-type tls_psk --key-data 303132333435363738393a3b3c3d3e3f
+ --key-type des --key-data 404142434445464748494a4b4c4d4e4f
+
+ """
+ if len(opts.key_type) != len(opts.key_data):
+ raise ValueError('There must be an equal number of key-type and key-data arguments')
+ kdb = []
+ for i in range(0, len(opts.key_type)):
+ if opts.key_check and len(opts.key_check) > i:
+ kcv = opts.key_check[i]
+ elif opts.suppress_key_check:
+ kcv = ''
+ else:
+ kcv_bin = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
+ kcv = b2h(kcv_bin)
+ if self._cmd.lchan.scc.scp:
+ # encrypte key data with DEK of current SCP
+ kcb = b2h(self._cmd.lchan.scc.scp.encrypt_key(h2b(opts.key_data[i])))
+ else:
+ # (for example) during personalization, DEK might not be required)
+ kcb = opts.key_data[i]
+ kdb.append({'key_type': opts.key_type[i], 'kcb': kcb, 'kcv': kcv})
+ p2 = opts.key_id
+ if len(opts.key_type) > 1:
+ p2 |= 0x80
+ self.put_key(opts.old_key_version_nr, opts.key_version_nr, p2, kdb)
+
+ # Table 11-68: Key Data Field - Format 1 (Basic Format)
+ KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
+ 'kcb'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
+ 'kcv'/HexAdapter(Prefixed(Int8ub, GreedyBytes))))
+
+ def put_key(self, old_kvn:int, kvn: int, kid: int, key_dict: dict) -> bytes:
+ """Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
+ See GlobalPlatform CardSpecification v2.3 Section 11.8 for details."""
+ key_data = kvn.to_bytes(1, 'big') + build_construct(ADF_SD.AddlShellCommands.KeyDataBasic, key_dict)
+ hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
+ data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data))
+ return data
+
+ get_status_parser = argparse.ArgumentParser()
+ get_status_parser.add_argument('subset', choices=StatusSubset.ksymapping.values(),
+ help='Subset of statuses to be included in the response')
+ get_status_parser.add_argument('--aid', type=is_hexstr, default='',
+ help='AID Search Qualifier (search only for given AID)')
+
+ @cmd2.with_argparser(get_status_parser)
+ def do_get_status(self, opts):
+ """Perform GlobalPlatform GET STATUS command in order to retrieve status information
+ on Issuer Security Domain, Executable Load File, Executable Module or Applications."""
+ grd_list = self.get_status(opts.subset, opts.aid)
+ for grd in grd_list:
+ self._cmd.poutput_json(grd.to_dict())
+
+ def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpRegistryRelatedData]:
+ subset_hex = b2h(build_construct(StatusSubset, subset))
+ aid = ApplicationAID(decoded=aid_search_qualifier)
+ cmd_data = aid.to_tlv() + h2b('5c054f9f70c5cc')
+ p2 = 0x02 # TLV format according to Table 11-36
+ grd_list = []
+ while True:
+ hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
+ data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data))
+ remainder = h2b(data)
+ while len(remainder):
+ # tlv sequence, each element is one GpRegistryRelatedData()
+ grd = GpRegistryRelatedData()
+ _dec, remainder = grd.from_tlv(remainder)
+ grd_list.append(grd)
+ if sw != '6310':
+ return grd_list
+ else:
+ p2 |= 0x01
+ return grd_list
+
+ set_status_parser = argparse.ArgumentParser()
+ set_status_parser.add_argument('scope', choices=SetStatusScope.ksymapping.values(),
+ help='Defines the scope of the requested status change')
+ set_status_parser.add_argument('status', choices=CLifeCycleState.ksymapping.values(),
+ help='Specify the new intended status')
+ set_status_parser.add_argument('--aid', type=is_hexstr,
+ help='AID of the target Application or Security Domain')
+
+ @cmd2.with_argparser(set_status_parser)
+ def do_set_status(self, opts):
+ """Perform GlobalPlatform SET STATUS command in order to change the life cycle state of the
+ Issuer Security Domain, Supplementary Security Domain or Application. This normally requires
+ prior authentication with a Secure Channel Protocol."""
+ self.set_status(opts.scope, opts.status, opts.aid)
+
+ def set_status(self, scope:str, status:str, aid:Hexstr = ''):
+ SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
+ 'scope'/SetStatusScope, 'status'/CLifeCycleState,
+ 'aid'/HexAdapter(Prefixed(Int8ub, COptional(GreedyBytes))))
+ apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
+ _data, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu))
+
+ inst_perso_parser = argparse.ArgumentParser()
+ inst_perso_parser.add_argument('application_aid', type=is_hexstr, help='Application AID')
+
+ @cmd2.with_argparser(inst_perso_parser)
+ def do_install_for_personalization(self, opts):
+ """Perform GlobalPlatform INSTALL [for personalization] command in order to inform a Security
+ Domain that the following STORE DATA commands are meant for a specific AID (specified here)."""
+ # Section 11.5.2.3.6 / Table 11-47
+ self.install(0x20, 0x00, "0000%02x%s000000" % (len(opts.application_aid)//2, opts.application_aid))
+
+ inst_inst_parser = argparse.ArgumentParser()
+ inst_inst_parser.add_argument('--load-file-aid', type=is_hexstr, default='',
+ help='Executable Load File AID')
+ inst_inst_parser.add_argument('--module-aid', type=is_hexstr, default='',
+ help='Executable Module AID')
+ inst_inst_parser.add_argument('--application-aid', type=is_hexstr, required=True,
+ help='Application AID')
+ inst_inst_parser.add_argument('--install-parameters', type=is_hexstr, default='',
+ help='Install Parameters')
+ inst_inst_parser.add_argument('--privilege', action='append', dest='privileges', default=[],
+ choices=Privileges._construct.flags.keys(),
+ help='Privilege granted to newly installed Application')
+ inst_inst_parser.add_argument('--install-token', type=is_hexstr, default='',
+ help='Install Token (Section GPCS C.4.2/C.4.7)')
+ inst_inst_parser.add_argument('--make-selectable', action='store_true',
+ help='Install and make selectable')
+
+ @cmd2.with_argparser(inst_inst_parser)
+ def do_install_for_install(self, opts):
+ """Perform GlobalPlatform INSTALL [for install] command in order to install an application."""
+ InstallForInstallCD = Struct('load_file_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
+ 'module_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
+ 'application_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
+ 'privileges'/Prefixed(Int8ub, Privileges._construct),
+ 'install_parameters'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
+ 'install_token'/HexAdapter(Prefixed(Int8ub, GreedyBytes)))
+ p1 = 0x04
+ if opts.make_selectable:
+ p1 |= 0x08
+ decoded = vars(opts)
+ # convert from list to "true-dict" as required by construct.FlagsEnum
+ decoded['privileges'] = {x: True for x in decoded['privileges']}
+ ifi_bytes = build_construct(InstallForInstallCD, decoded)
+ self.install(p1, 0x00, b2h(ifi_bytes))
+
+ def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
+ cmd_hex = "80E6%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
+ return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
+
+ del_cc_parser = argparse.ArgumentParser()
+ del_cc_parser.add_argument('aid', type=is_hexstr,
+ help='Executable Load File or Application AID')
+ del_cc_parser.add_argument('--delete-related-objects', action='store_true',
+ help='Delete not only the object but also its related objects')
+
+ @cmd2.with_argparser(del_cc_parser)
+ def do_delete_card_content(self, opts):
+ """Perform a GlobalPlatform DELETE [card content] command in order to delete an Executable Load
+ File, an Application or an Executable Load File and its related Applications."""
+ p2 = 0x80 if opts.delete_related_objects else 0x00
+ aid = ApplicationAID(decoded=opts.aid)
+ self.delete(0x00, p2, b2h(aid.to_tlv()))
+
+ del_key_parser = argparse.ArgumentParser()
+ del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)')
+ del_key_parser.add_argument('--key-ver', type=auto_uint8, help='Key Version Number (KVN)')
+ del_key_parser.add_argument('--delete-related-objects', action='store_true',
+ help='Delete not only the object but also its related objects')
+
+ @cmd2.with_argparser(del_key_parser)
+ def do_delete_key(self, opts):
+ """Perform GlobalPlaform DELETE (Key) command.
+ If both KID and KVN are specified, exactly one key is deleted. If only either of the two is
+ specified, multiple matching keys may be deleted."""
+ if opts.key_id is None and opts.key_ver is None:
+ raise ValueError('At least one of KID or KVN must be specified')
+ p2 = 0x80 if opts.delete_related_objects else 0x00
+ cmd = ""
+ if opts.key_id is not None:
+ cmd += "d001%02x" % opts.key_id
+ if opts.key_ver is not None:
+ cmd += "d201%02x" % opts.key_ver
+ self.delete(0x00, p2, cmd)
+
+ def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
+ cmd_hex = "80E4%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
+ return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
+
+ est_scp02_parser = argparse.ArgumentParser()
+ est_scp02_parser.add_argument('--key-ver', type=auto_uint8, required=True, help='Key Version Number (KVN)')
+ est_scp02_parser.add_argument('--host-challenge', type=is_hexstr,
+ help='Hard-code the host challenge; default: random')
+ est_scp02_parser.add_argument('--security-level', type=auto_uint8, default=0x01,
+ help='Security Level. Default: 0x01 (C-MAC only)')
+ est_scp02_p_k = est_scp02_parser.add_argument_group('Manual key specification')
+ est_scp02_p_k.add_argument('--key-enc', type=is_hexstr, help='Secure Channel Encryption Key')
+ est_scp02_p_k.add_argument('--key-mac', type=is_hexstr, help='Secure Channel MAC Key')
+ est_scp02_p_k.add_argument('--key-dek', type=is_hexstr, help='Data Encryption Key')
+ est_scp02_p_csv = est_scp02_parser.add_argument_group('Obtain keys from CardKeyProvider (e.g. CSV')
+ est_scp02_p_csv.add_argument('--key-provider-suffix', help='Suffix for key names in CardKeyProvider')
+
+ @cmd2.with_argparser(est_scp02_parser)
+ def do_establish_scp02(self, opts):
+ """Establish a secure channel using the GlobalPlatform SCP02 protocol. It can be released
+ again by using `release_scp`."""
+ if opts.key_provider_suffix:
+ suffix = opts.key_provider_suffix
+ id_field_name = self._cmd.lchan.selected_adf.scp_key_identity
+ identity = self._cmd.rs.identity.get(id_field_name)
+ opts.key_enc = card_key_provider_get_field('SCP02_ENC_' + suffix, key=id_field_name, value=identity)
+ opts.key_mac = card_key_provider_get_field('SCP02_MAC_' + suffix, key=id_field_name, value=identity)
+ opts.key_dek = card_key_provider_get_field('SCP02_DEK_' + suffix, key=id_field_name, value=identity)
+ else:
+ if not opts.key_enc or not opts.key_mac:
+ self._cmd.poutput("Cannot establish SCP02 without at least ENC and MAC keys given!")
+ return
+ if self._cmd.lchan.scc.scp:
+ self._cmd.poutput("Cannot establish SCP02 as this lchan already has a SCP instance!")
+ return
+ host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(8)
+ kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
+ scp02 = SCP02(card_keys=kset)
+ self._establish_scp(scp02, host_challenge, opts.security_level)
+
+ est_scp03_parser = deepcopy(est_scp02_parser)
+ est_scp03_parser.add_argument('--s16-mode', action='store_true', help='S16 mode (S8 is default)')
+
+ @cmd2.with_argparser(est_scp03_parser)
+ def do_establish_scp03(self, opts):
+ """Establish a secure channel using the GlobalPlatform SCP03 protocol. It can be released
+ again by using `release_scp`."""
+ if opts.key_provider_suffix:
+ suffix = opts.key_provider_suffix
+ id_field_name = self._cmd.lchan.selected_adf.scp_key_identity
+ identity = self._cmd.rs.identity.get(id_field_name)
+ opts.key_enc = card_key_provider_get_field('SCP03_ENC_' + suffix, key=id_field_name, value=identity)
+ opts.key_mac = card_key_provider_get_field('SCP03_MAC_' + suffix, key=id_field_name, value=identity)
+ opts.key_dek = card_key_provider_get_field('SCP03_DEK_' + suffix, key=id_field_name, value=identity)
+ else:
+ if not opts.key_enc or not opts.key_mac:
+ self._cmd.poutput("Cannot establish SCP03 without at least ENC and MAC keys given!")
+ return
+ if self._cmd.lchan.scc.scp:
+ self._cmd.poutput("Cannot establish SCP03 as this lchan already has a SCP instance!")
+ return
+ s_mode = 16 if opts.s16_mode else 8
+ host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(s_mode)
+ kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
+ scp03 = SCP03(card_keys=kset, s_mode = s_mode)
+ self._establish_scp(scp03, host_challenge, opts.security_level)
+
+ def _establish_scp(self, scp, host_challenge, security_level):
+ # perform the common functionality shared by SCP02 and SCP03 establishment
+ init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge)
+ init_update_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(init_update_apdu))
+ scp.parse_init_update_resp(h2b(init_update_resp))
+ ext_auth_apdu = scp.gen_ext_auth_apdu(security_level)
+ _ext_auth_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(ext_auth_apdu))
+ self._cmd.poutput("Successfully established a %s secure channel" % str(scp))
+ # store a reference to the SCP instance
+ self._cmd.lchan.scc.scp = scp
+ self._cmd.update_prompt()
+
+
+ def do_release_scp(self, _opts):
+ """Release a previously establiehed secure channel."""
+ if not self._cmd.lchan.scc.scp:
+ self._cmd.poutput("Cannot release SCP as none is established")
+ return
+ self._cmd.lchan.scc.scp = None
+ self._cmd.update_prompt()
+
+
+# Card Application of a Security Domain
+class CardApplicationSD(CardApplication):
+ __intermediate = True
+ def __init__(self, aid: str, name: str, desc: str):
+ super().__init__(name, adf=ADF_SD(aid, name, desc), sw=sw_table)
+ # the identity (e.g. 'ICCID', 'EID') that should be used as a look-up key to attempt to retrieve
+ # the key material for the security domain from the CardKeyProvider
+ self.adf.scp_key_identity = None
+
+# Card Application of Issuer Security Domain
+class CardApplicationISD(CardApplicationSD):
+ # FIXME: ISD AID is not static, but could be different. One can select the empty
+ # application using '00a4040000' and then parse the response FCI to get the ISD AID
+ def __init__(self, aid='a000000003000000'):
+ super().__init__(aid=aid, name='ADF.ISD', desc='Issuer Security Domain')
+
+#class CardProfileGlobalPlatform(CardProfile):
+# ORDER = 23
+#
+# def __init__(self, name='GlobalPlatform'):
+# super().__init__(name, desc='GlobalPlatfomr 2.1.1', cla=['00','80','84'], sw=sw_table)
+
+
+class GpCardKeyset:
+ """A single set of GlobalPlatform card keys and the associated KVN."""
+ def __init__(self, kvn: int, enc: bytes, mac: bytes, dek: bytes):
+ assert 0 < kvn < 256
+ assert len(enc) == len(mac) == len(dek)
+ self.kvn = kvn
+ self.enc = enc
+ self.mac = mac
+ self.dek = dek
+
+ @classmethod
+ def from_single_key(cls, kvn: int, base_key: bytes) -> 'GpCardKeyset':
+ return cls(kvn, base_key, base_key, base_key)
+
+ def __str__(self):
+ return "%s(KVN=%u, ENC=%s, MAC=%s, DEK=%s)" % (self.__class__.__name__,
+ self.kvn, b2h(self.enc), b2h(self.mac), b2h(self.dek))
+
+
+def compute_kcv_des(key:bytes) -> bytes:
+ # GP Card Spec B.6: For a DES key, the key check value is computed by encrypting 8 bytes, each with
+ # value '00', with the key to be checked and retaining the 3 highest-order bytes of the encrypted
+ # result.
+ plaintext = b'\x00' * 8
+ cipher = DES3.new(key, DES.MODE_ECB)
+ return cipher.encrypt(plaintext)
+
+def compute_kcv_aes(key:bytes) -> bytes:
+ # GP Card Spec B.6: For a AES key, the key check value is computed by encrypting 16 bytes, each with
+ # value '01', with the key to be checked and retaining the 3 highest-order bytes of the encrypted
+ # result.
+ plaintext = b'\x01' * 16
+ cipher = AES.new(key, AES.MODE_ECB)
+ return cipher.encrypt(plaintext)
+
+# dict is keyed by the string name of the KeyType enum above in this file
+KCV_CALCULATOR = {
+ 'aes': compute_kcv_aes,
+ 'des': compute_kcv_des,
+ }
+
+def compute_kcv(key_type: str, key: bytes) -> Optional[bytes]:
+ """Compute the KCV (Key Check Value) for given key type and key."""
+ kcv_calculator = KCV_CALCULATOR.get(key_type)
+ if not kcv_calculator:
+ return None
+ else:
+ return kcv_calculator(key)[:3]
diff --git a/pySim/global_platform/scp.py b/pySim/global_platform/scp.py
new file mode 100644
index 0000000..967a582
--- /dev/null
+++ b/pySim/global_platform/scp.py
@@ -0,0 +1,534 @@
+# Global Platform SCP02 + SCP03 (Secure Channel Protocol) implementation
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import abc
+import logging
+from typing import Optional
+from Cryptodome.Cipher import DES3, DES
+from Cryptodome.Util.strxor import strxor
+from construct import Struct, Bytes, Int8ub, Int16ub, Const
+from construct import Optional as COptional
+from pySim.utils import b2h, bertlv_parse_len, bertlv_encode_len
+from pySim.secure_channel import SecureChannel
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+def scp02_key_derivation(constant: bytes, counter: int, base_key: bytes) -> bytes:
+ assert len(constant) == 2
+ assert(counter >= 0 and counter <= 65535)
+ assert len(base_key) == 16
+
+ derivation_data = constant + counter.to_bytes(2, 'big') + b'\x00' * 12
+ cipher = DES3.new(base_key, DES.MODE_CBC, b'\x00' * 8)
+ return cipher.encrypt(derivation_data)
+
+# TODO: resolve duplication with BspAlgoCryptAES128
+def pad80(s: bytes, BS=8) -> bytes:
+ """ Pad bytestring s: add '\x80' and '\0'* so the result to be multiple of BS."""
+ l = BS-1 - len(s) % BS
+ return s + b'\x80' + b'\0'*l
+
+# TODO: resolve duplication with BspAlgoCryptAES128
+def unpad80(padded: bytes) -> bytes:
+ """Remove the customary 80 00 00 ... padding used for AES."""
+ # first remove any trailing zero bytes
+ stripped = padded.rstrip(b'\0')
+ # then remove the final 80
+ assert stripped[-1] == 0x80
+ return stripped[:-1]
+
+class Scp02SessionKeys:
+ """A single set of GlobalPlatform session keys."""
+ DERIV_CONST_CMAC = b'\x01\x01'
+ DERIV_CONST_RMAC = b'\x01\x02'
+ DERIV_CONST_ENC = b'\x01\x82'
+ DERIV_CONST_DENC = b'\x01\x81'
+ blocksize = 8
+
+ def calc_mac_1des(self, data: bytes, reset_icv: bool = False) -> bytes:
+ """Pad and calculate MAC according to B.1.2.2 - Single DES plus final 3DES"""
+ e = DES.new(self.c_mac[:8], DES.MODE_ECB)
+ d = DES.new(self.c_mac[8:], DES.MODE_ECB)
+ padded_data = pad80(data, 8)
+ q = len(padded_data) // 8
+ icv = b'\x00' * 8 if reset_icv else self.icv
+ h = icv
+ for i in range(q):
+ h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
+ h = d.decrypt(h)
+ h = e.encrypt(h)
+ logger.debug("mac_1des(%s,icv=%s) -> %s", b2h(data), b2h(icv), b2h(h))
+ if self.des_icv_enc:
+ self.icv = self.des_icv_enc.encrypt(h)
+ else:
+ self.icv = h
+ return h
+
+ def calc_mac_3des(self, data: bytes) -> bytes:
+ e = DES3.new(self.enc, DES.MODE_ECB)
+ padded_data = pad80(data, 8)
+ q = len(padded_data) // 8
+ h = b'\x00' * 8
+ for i in range(q):
+ h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
+ logger.debug("mac_3des(%s) -> %s", b2h(data), b2h(h))
+ return h
+
+ def __init__(self, counter: int, card_keys: 'GpCardKeyset', icv_encrypt=True):
+ self.icv = None
+ self.counter = counter
+ self.card_keys = card_keys
+ self.c_mac = scp02_key_derivation(self.DERIV_CONST_CMAC, self.counter, card_keys.mac)
+ self.r_mac = scp02_key_derivation(self.DERIV_CONST_RMAC, self.counter, card_keys.mac)
+ self.enc = scp02_key_derivation(self.DERIV_CONST_ENC, self.counter, card_keys.enc)
+ self.data_enc = scp02_key_derivation(self.DERIV_CONST_DENC, self.counter, card_keys.dek)
+ self.des_icv_enc = DES.new(self.c_mac[:8], DES.MODE_ECB) if icv_encrypt else None
+
+ def __str__(self) -> str:
+ return "%s(CTR=%u, ICV=%s, ENC=%s, D-ENC=%s, MAC-C=%s, MAC-R=%s)" % (
+ self.__class__.__name__, self.counter, b2h(self.icv) if self.icv else "None",
+ b2h(self.enc), b2h(self.data_enc), b2h(self.c_mac), b2h(self.r_mac))
+
+INS_INIT_UPDATE = 0x50
+INS_EXT_AUTH = 0x82
+CLA_SM = 0x04
+
+class SCP(SecureChannel, abc.ABC):
+ """Abstract base class containing some common interface + functionality for SCP protocols."""
+ def __init__(self, card_keys: 'GpCardKeyset', lchan_nr: int = 0):
+ if hasattr(self, 'kvn_range'):
+ if not card_keys.kvn in range(self.kvn_range[0], self.kvn_range[1]+1):
+ raise ValueError('%s cannot be used with KVN outside range 0x%02x..0x%02x' %
+ (self.__class__.__name__, self.kvn_range[0], self.kvn_range[1]))
+ self.lchan_nr = lchan_nr
+ self.card_keys = card_keys
+ self.sk = None
+ self.mac_on_unmodified = False
+ self.security_level = 0x00
+
+ @property
+ def do_cmac(self) -> bool:
+ """Should we perform C-MAC?"""
+ return self.security_level & 0x01
+
+ @property
+ def do_rmac(self) -> bool:
+ """Should we perform R-MAC?"""
+ return self.security_level & 0x10
+
+ @property
+ def do_cenc(self) -> bool:
+ """Should we perform C-ENC?"""
+ return self.security_level & 0x02
+
+ @property
+ def do_renc(self) -> bool:
+ """Should we perform R-ENC?"""
+ return self.security_level & 0x20
+
+ def __str__(self) -> str:
+ return "%s[%02x]" % (self.__class__.__name__, self.security_level)
+
+ def _cla(self, sm: bool = False, b8: bool = True) -> int:
+ ret = 0x80 if b8 else 0x00
+ if sm:
+ ret = ret | CLA_SM
+ return ret + self.lchan_nr
+
+ def wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
+ # Generic handling of GlobalPlatform SCP, implements SecureChannel.wrap_cmd_apdu
+ # only protect those APDUs that actually are global platform commands
+ if apdu[0] & 0x80:
+ return self._wrap_cmd_apdu(apdu, *args, **kwargs)
+ return apdu
+
+ @abc.abstractmethod
+ def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
+ """Method implementation to be provided by derived class."""
+ pass
+
+ @abc.abstractmethod
+ def gen_init_update_apdu(self, host_challenge: Optional[bytes]) -> bytes:
+ pass
+
+ @abc.abstractmethod
+ def parse_init_update_resp(self, resp_bin: bytes):
+ pass
+
+ @abc.abstractmethod
+ def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
+ pass
+
+ def encrypt_key(self, key: bytes) -> bytes:
+ """Encrypt a key with the DEK."""
+ num_pad = len(key) % self.sk.blocksize
+ if num_pad:
+ return bertlv_encode_len(len(key)) + self.dek_encrypt(key + b'\x00'*num_pad)
+ return self.dek_encrypt(key)
+
+ def decrypt_key(self, encrypted_key:bytes) -> bytes:
+ """Decrypt a key with the DEK."""
+ if len(encrypted_key) % self.sk.blocksize:
+ # If the length of the Key Component Block is not a multiple of the block size of the encryption #
+ # algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that the key
+ # component value was right-padded prior to encryption and that the Key Component Block was
+ # formatted as described in Table 11-70. In this case, the first byte(s) of the Key Component
+ # Block provides the actual length of the key component value, which allows recovering the
+ # clear-text key component value after decryption of the encrypted key component value and removal
+ # of padding bytes.
+ decrypted = self.dek_decrypt(encrypted_key)
+ key_len, remainder = bertlv_parse_len(decrypted)
+ return remainder[:key_len]
+ else:
+ # If the length of the Key Component Block is a multiple of the block size of the encryption
+ # algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that no padding
+ # bytes were added before encrypting the key component value and that the Key Component Block is
+ # only composed of the encrypted key component value (as shown in Table 11-71). In this case, the
+ # clear-text key component value is simply recovered by decrypting the Key Component Block.
+ return self.dek_decrypt(encrypted_key)
+
+ @abc.abstractmethod
+ def dek_encrypt(self, plaintext:bytes) -> bytes:
+ pass
+
+ @abc.abstractmethod
+ def dek_decrypt(self, ciphertext:bytes) -> bytes:
+ pass
+
+
+class SCP02(SCP):
+ """An instance of the GlobalPlatform SCP02 secure channel protocol."""
+
+ constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x02'),
+ 'seq_counter'/Int16ub, 'card_challenge'/Bytes(6), 'card_cryptogram'/Bytes(8))
+ kvn_range = [0x20, 0x2f]
+
+ def __init__(self, *args, **kwargs):
+ self.overhead = 8
+ super().__init__(*args, **kwargs)
+
+ def dek_encrypt(self, plaintext:bytes) -> bytes:
+ cipher = DES.new(self.card_keys.dek, DES.MODE_ECB)
+ return cipher.encrypt(plaintext)
+
+ def dek_decrypt(self, ciphertext:bytes) -> bytes:
+ cipher = DES.new(self.card_keys.dek, DES.MODE_ECB)
+ return cipher.decrypt(ciphertext)
+
+ def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
+ logger.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge))
+ self.host_cryptogram = self.sk.calc_mac_3des(self.sk.counter.to_bytes(2, 'big') + card_challenge + host_challenge)
+ self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2, 'big') + card_challenge)
+ logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
+
+ def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes:
+ """Generate INITIALIZE UPDATE APDU."""
+ self.host_challenge = host_challenge
+ return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge
+
+ def parse_init_update_resp(self, resp_bin: bytes):
+ """Parse response to INITIALZIE UPDATE."""
+ resp = self.constr_iur.parse(resp_bin)
+ self.card_challenge = resp['card_challenge']
+ self.sk = Scp02SessionKeys(resp['seq_counter'], self.card_keys)
+ logger.debug(self.sk)
+ self._compute_cryptograms(self.card_challenge, self.host_challenge)
+ if self.card_cryptogram != resp['card_cryptogram']:
+ raise ValueError("card cryptogram doesn't match")
+
+ def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
+ """Generate EXTERNAL AUTHENTICATE APDU."""
+ if security_level & 0xf0:
+ raise NotImplementedError('R-MAC/R-ENC for SCP02 not implemented yet.')
+ self.security_level = security_level
+ if self.mac_on_unmodified:
+ header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, 8])
+ else:
+ header = bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16])
+ #return self.wrap_cmd_apdu(header + self.host_cryptogram)
+ mac = self.sk.calc_mac_1des(header + self.host_cryptogram, True)
+ return bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16]) + self.host_cryptogram + mac
+
+ def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
+ """Wrap Command APDU for SCP02: calculate MAC and encrypt."""
+ lc = len(apdu) - 5
+ assert len(apdu) >= 5, "Wrong APDU length: %d" % len(apdu)
+ assert len(apdu) == 5 or apdu[4] == lc, "Lc differs from length of data: %d vs %d" % (apdu[4], lc)
+
+ logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
+
+ cla = apdu[0]
+ b8 = cla & 0x80
+ if cla & 0x03 or cla & CLA_SM:
+ # nonzero logical channel in APDU, check that are the same
+ assert cla == self._cla(False, b8), "CLA mismatch"
+ # CLA without log. channel can be 80 or 00 only
+ if self.do_cmac:
+ if self.mac_on_unmodified:
+ mlc = lc
+ clac = cla
+ else: # CMAC on modified APDU
+ mlc = lc + 8
+ clac = cla | CLA_SM
+ mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + apdu[5:])
+ if self.do_cenc:
+ k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
+ data = k.encrypt(pad80(apdu[5:], 8))
+ lc = len(data)
+ else:
+ data = apdu[5:]
+ lc += 8
+ apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
+ return apdu
+
+ def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
+ # TODO: Implement R-MAC / R-ENC
+ return rsp_apdu
+
+
+
+from Cryptodome.Cipher import AES
+from Cryptodome.Hash import CMAC
+
+def scp03_key_derivation(constant: bytes, context: bytes, base_key: bytes, l: Optional[int] = None) -> bytes:
+ """SCP03 Key Derivation Function as specified in Annex D 4.1.5."""
+ # Data derivation shall use KDF in counter mode as specified in NIST SP 800-108 ([NIST 800-108]). The PRF
+ # used in the KDF shall be CMAC as specified in [NIST 800-38B], used with full 16-byte output length.
+ def prf(key: bytes, data:bytes):
+ return CMAC.new(key, data, AES).digest()
+
+ if l is None:
+ l = len(base_key) * 8
+
+ logger.debug("scp03_kdf(constant=%s, context=%s, base_key=%s, l=%u)", b2h(constant), b2h(context), b2h(base_key), l)
+ output_len = l // 8
+ # SCP03 Section 4.1.5 defines a different parameter order than NIST SP 800-108, so we cannot use the
+ # existing Cryptodome.Protocol.KDF.SP800_108_Counter function :(
+ # A 12-byte “label†consisting of 11 bytes with value '00' followed by a 1-byte derivation constant
+ assert len(constant) == 1
+ label = b'\x00' *11 + constant
+ i = 1
+ dk = b''
+ while len(dk) < output_len:
+ # 12B label, 1B separation, 2B L, 1B i, Context
+ info = label + b'\x00' + l.to_bytes(2, 'big') + bytes([i]) + context
+ dk += prf(base_key, info)
+ i += 1
+ if i > 0xffff:
+ raise ValueError("Overflow in SP800 108 counter")
+ return dk[:output_len]
+
+
+class Scp03SessionKeys:
+ # GPC 2.3 Amendment D v1.2 Section 4.1.5 Table 4-1
+ DERIV_CONST_AUTH_CGRAM_CARD = b'\x00'
+ DERIV_CONST_AUTH_CGRAM_HOST = b'\x01'
+ DERIV_CONST_CARD_CHLG_GEN = b'\x02'
+ DERIV_CONST_KDERIV_S_ENC = b'\x04'
+ DERIV_CONST_KDERIV_S_MAC = b'\x06'
+ DERIV_CONST_KDERIV_S_RMAC = b'\x07'
+ blocksize = 16
+
+ def __init__(self, card_keys: 'GpCardKeyset', host_challenge: bytes, card_challenge: bytes):
+ # GPC 2.3 Amendment D v1.2 Section 6.2.1
+ context = host_challenge + card_challenge
+ self.s_enc = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_ENC, context, card_keys.enc)
+ self.s_mac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_MAC, context, card_keys.mac)
+ self.s_rmac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_RMAC, context, card_keys.mac)
+
+
+ # The first MAC chaining value is set to 16 bytes '00'
+ self.mac_chaining_value = b'\x00' * 16
+ # The encryption counter’s start value shall be set to 1 (we set it immediately before generating ICV)
+ self.block_nr = 0
+
+ def calc_cmac(self, apdu: bytes):
+ """Compute C-MAC for given to-be-transmitted APDU.
+ Returns the full 16-byte MAC, caller must truncate it if needed for S8 mode."""
+ cmac_input = self.mac_chaining_value + apdu
+ cmac_val = CMAC.new(self.s_mac, cmac_input, ciphermod=AES).digest()
+ self.mac_chaining_value = cmac_val
+ return cmac_val
+
+ def calc_rmac(self, rdata_and_sw: bytes):
+ """Compute R-MAC for given received R-APDU data section.
+ Returns the full 16-byte MAC, caller must truncate it if needed for S8 mode."""
+ rmac_input = self.mac_chaining_value + rdata_and_sw
+ return CMAC.new(self.s_rmac, rmac_input, ciphermod=AES).digest()
+
+ def _get_icv(self, is_response: bool = False):
+ """Obtain the ICV value computed as described in 6.2.6.
+ 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 R-DEC."""
+ if not is_response:
+ self.block_nr += 1
+ # The binary value of this number SHALL be left padded with zeroes to form a full block.
+ data = self.block_nr.to_bytes(self.blocksize, "big")
+ if is_response:
+ # Section 6.2.7: additional intermediate step: Before encryption, the most significant byte of
+ # this block shall be set to '80'.
+ data = b'\x80' + data[1:]
+ iv = bytes([0] * self.blocksize)
+ # This block SHALL be encrypted with S-ENC to produce the ICV for command encryption.
+ cipher = AES.new(self.s_enc, AES.MODE_CBC, iv)
+ icv = cipher.encrypt(data)
+ logger.debug("_get_icv(data=%s, is_resp=%s) -> icv=%s", b2h(data), is_response, b2h(icv))
+ return icv
+
+ # TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-wrapping
+ def _encrypt(self, data: bytes, is_response: bool = False) -> bytes:
+ cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv(is_response))
+ return cipher.encrypt(data)
+
+ # TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-unwrapping
+ def _decrypt(self, data: bytes, is_response: bool = True) -> bytes:
+ cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv(is_response))
+ return cipher.decrypt(data)
+
+
+class SCP03(SCP):
+ """Secure Channel Protocol (SCP) 03 as specified in GlobalPlatform v2.3 Amendment D."""
+
+ # Section 7.1.1.6 / Table 7-3
+ constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x03'), 'i_param'/Int8ub,
+ 'card_challenge'/Bytes(lambda ctx: ctx._.s_mode),
+ 'card_cryptogram'/Bytes(lambda ctx: ctx._.s_mode),
+ 'sequence_counter'/COptional(Bytes(3)))
+ kvn_range = [0x30, 0x3f]
+
+ def __init__(self, *args, **kwargs):
+ self.s_mode = kwargs.pop('s_mode', 8)
+ self.overhead = self.s_mode
+ super().__init__(*args, **kwargs)
+
+ def dek_encrypt(self, plaintext:bytes) -> bytes:
+ cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
+ return cipher.encrypt(plaintext)
+
+ def dek_decrypt(self, ciphertext:bytes) -> bytes:
+ cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
+ return cipher.decrypt(ciphertext)
+
+ def _compute_cryptograms(self):
+ logger.debug("host_challenge(%s), card_challenge(%s)", b2h(self.host_challenge), b2h(self.card_challenge))
+ # Card + Host Authentication Cryptogram: Section 6.2.2.2 + 6.2.2.3
+ context = self.host_challenge + self.card_challenge
+ self.card_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_CARD, context, self.sk.s_mac, l=self.s_mode*8)
+ self.host_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_HOST, context, self.sk.s_mac, l=self.s_mode*8)
+ logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
+
+ def gen_init_update_apdu(self, host_challenge: Optional[bytes] = None) -> bytes:
+ """Generate INITIALIZE UPDATE APDU."""
+ if host_challenge is None:
+ host_challenge = b'\x00' * self.s_mode
+ if len(host_challenge) != self.s_mode:
+ raise ValueError('Host Challenge must be %u bytes long' % self.s_mode)
+ self.host_challenge = host_challenge
+ return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge
+
+ def parse_init_update_resp(self, resp_bin: bytes):
+ """Parse response to INITIALIZE UPDATE."""
+ if len(resp_bin) not in [10+3+8+8, 10+3+16+16, 10+3+8+8+3, 10+3+16+16+3]:
+ raise ValueError('Invalid length of Initialize Update Response')
+ resp = self.constr_iur.parse(resp_bin, s_mode=self.s_mode)
+ self.card_challenge = resp['card_challenge']
+ self.i_param = resp['i_param']
+ # derive session keys and compute cryptograms
+ self.sk = Scp03SessionKeys(self.card_keys, self.host_challenge, self.card_challenge)
+ logger.debug(self.sk)
+ self._compute_cryptograms()
+ # verify computed cryptogram matches received cryptogram
+ if self.card_cryptogram != resp['card_cryptogram']:
+ raise ValueError("card cryptogram doesn't match")
+
+ def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
+ """Generate EXTERNAL AUTHENTICATE APDU."""
+ self.security_level = security_level
+ header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, self.s_mode])
+ # bypass encryption for EXTERNAL AUTHENTICATE
+ return self.wrap_cmd_apdu(header + self.host_cryptogram, skip_cenc=True)
+
+ def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes:
+ """Wrap Command APDU for SCP02: calculate MAC and encrypt."""
+ cla = apdu[0]
+ ins = apdu[1]
+ p1 = apdu[2]
+ p2 = apdu[3]
+ lc = apdu[4]
+ assert lc == len(apdu) - 5
+ cmd_data = apdu[5:]
+
+ if self.do_cenc and not skip_cenc:
+ assert self.do_cmac
+ if lc == 0:
+ # No encryption shall be applied to a command where there is no command data field. In this
+ # case, the encryption counter shall still be incremented
+ self.sk.block_nr += 1
+ else:
+ # data shall be padded as defined in [GPCS] section B.2.3
+ padded_data = pad80(cmd_data, 16)
+ lc = len(padded_data)
+ if lc >= 256:
+ raise ValueError('Modified Lc (%u) would exceed maximum when appending padding' % (lc))
+ # perform AES-CBC with ICV + S_ENC
+ cmd_data = self.sk._encrypt(padded_data)
+
+ if self.do_cmac:
+ # The length of the command message (Lc) shall be incremented by 8 (in S8 mode) or 16 (in S16
+ # mode) to indicate the inclusion of the C-MAC in the data field of the command message.
+ mlc = lc + self.s_mode
+ if mlc >= 256:
+ raise ValueError('Modified Lc (%u) would exceed maximum when appending %u bytes of mac' % (mlc, self.s_mode))
+ # The class byte shall be modified for the generation or verification of the C-MAC: The logical
+ # channel number shall be set to zero, bit 4 shall be set to 0 and bit 3 shall be set to 1 to indicate
+ # GlobalPlatform proprietary secure messaging.
+ mcla = (cla & 0xF0) | CLA_SM
+ mapdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
+ cmac = self.sk.calc_cmac(mapdu)
+ mapdu += cmac[:self.s_mode]
+
+ return mapdu
+
+ def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
+ # No R-MAC shall be generated and no protection shall be applied to a response that includes an error
+ # status word: in this case only the status word shall be returned in the response. All status words
+ # except '9000' and warning status words (i.e. '62xx' and '63xx') shall be interpreted as error status
+ # words.
+ logger.debug("unwrap_rsp_apdu(sw=%s, rsp_apdu=%s)", sw, rsp_apdu)
+ if not self.do_rmac:
+ assert not self.do_renc
+ return rsp_apdu
+
+ if sw != b'\x90\x00' and sw[0] not in [0x62, 0x63]:
+ return rsp_apdu
+ response_data = rsp_apdu[:-self.s_mode]
+ rmac = rsp_apdu[-self.s_mode:]
+ rmac_exp = self.sk.calc_rmac(response_data + sw)[:self.s_mode]
+ if rmac != rmac_exp:
+ raise ValueError("R-MAC value not matching: received: %s, computed: %s" % (rmac, rmac_exp))
+
+ if self.do_renc:
+ # decrypt response data
+ decrypted = self.sk._decrypt(response_data)
+ logger.debug("decrypted: %s", b2h(decrypted))
+ # remove padding
+ response_data = unpad80(decrypted)
+ logger.debug("response_data: %s", b2h(response_data))
+
+ return response_data
diff --git a/pySim/global_platform/uicc.py b/pySim/global_platform/uicc.py
new file mode 100644
index 0000000..2cf5e96
--- /dev/null
+++ b/pySim/global_platform/uicc.py
@@ -0,0 +1,107 @@
+# coding=utf-8
+"""GlobalPLatform UICC Configuration 1.0 parameters
+
+(C) 2024 by Harald Welte <laforge@osmocom.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from construct import Optional as COptional
+from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
+from pySim.construct import *
+from pySim.utils import *
+from pySim.tlv import *
+
+# Section 11.6.2.3 / Table 11-58
+class SecurityDomainAid(BER_TLV_IE, tag=0x4f):
+ _construct = GreedyBytes
+class LoadFileDataBlockSignature(BER_TLV_IE, tag=0xc3):
+ _construct = GreedyBytes
+class DapBlock(BER_TLV_IE, tag=0xe2, nested=[SecurityDomainAid, LoadFileDataBlockSignature]):
+ pass
+class LoadFileDataBlock(BER_TLV_IE, tag=0xc4):
+ _construct = GreedyBytes
+class Icv(BER_TLV_IE, tag=0xd3):
+ _construct = GreedyBytes
+class CipheredLoadFileDataBlock(BER_TLV_IE, tag=0xd4):
+ _construct = GreedyBytes
+class LoadFile(TLV_IE_Collection, nested=[DapBlock, LoadFileDataBlock, Icv, CipheredLoadFileDataBlock]):
+ pass
+
+# UICC Configuration v1.0.1 / Section 4.3.2
+class UiccScp(BER_TLV_IE, tag=0x81):
+ _construct = Struct('scp'/Int8ub, 'i'/Int8ub)
+
+class AcceptExtradAppsAndElfToSd(BER_TLV_IE, tag=0x82):
+ _construct = GreedyBytes
+
+class AcceptDelOfAssocSd(BER_TLV_IE, tag=0x83):
+ _construct = GreedyBytes
+
+class LifeCycleTransitionToPersonalized(BER_TLV_IE, tag=0x84):
+ _construct = GreedyBytes
+
+class CasdCapabilityInformation(BER_TLV_IE, tag=0x86):
+ _construct = GreedyBytes
+
+class AcceptExtradAssocAppsAndElf(BER_TLV_IE, tag=0x87):
+ _construct = GreedyBytes
+
+# Security Domain Install Parameters (inside C9 during INSTALL [for install])
+class UiccSdInstallParams(TLV_IE_Collection, nested=[UiccScp, AcceptExtradAppsAndElfToSd, AcceptDelOfAssocSd,
+ LifeCycleTransitionToPersonalized,
+ CasdCapabilityInformation, AcceptExtradAssocAppsAndElf]):
+ def has_scp(self, scp: int) -> bool:
+ """Determine if SD Installation parameters already specify given SCP."""
+ for c in self.children:
+ if not isinstance(c, UiccScp):
+ continue
+ if c.decoded['scp'] == scp:
+ return True
+ return False
+
+ def add_scp(self, scp: int, i: int):
+ """Add given SCP (and i parameter) to list of SCP of the Security Domain Install Params.
+ Example: add_scp(0x03, 0x70) for SCP03, or add_scp(0x02, 0x55) for SCP02."""
+ if self.has_scp(scp):
+ raise ValueError('SCP%02x already present' % scp)
+ self.children.append(UiccScp(decoded={'scp': scp, 'i': i}))
+
+ def remove_scp(self, scp: int):
+ """Remove given SCP from list of SCP of the Security Domain Install Params."""
+ for c in self.children:
+ if not isinstance(c, UiccScp):
+ continue
+ if c.decoded['scp'] == scp:
+ self.children.remove(c)
+ return
+ raise ValueError("SCP%02x not present" % scp)
+
+
+# Key Usage:
+# KVN 0x01 .. 0x0F reserved for SCP80
+# KVN 0x11 reserved for DAP specified in ETSI TS 102 226
+# KVN 0x20 .. 0x2F reserved for SCP02
+# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
+# KVN 0x30 .. 0x3F reserved for SCP03
+# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
+# KVN 0x70 KID 0x01: Token key (RSA public or DES)
+# KVN 0x71 KID 0x01: Receipt key (DES)
+# KVN 0x73 KID 0x01: DAP verifiation key (RS public or DES)
+# KVN 0x74 reserved for CASD
+# KID 0x01: PK.CA.AUT
+# KID 0x02: SK.CASD.AUT (PK) and KS.CASD.AUT (Non-PK)
+# KID 0x03: SK.CASD.CT (P) and KS.CASD.CT (Non-PK)
+# KVN 0x75 KID 0x01: 16-byte DES key for Ciphered Load File Data Block
+# KVN 0xFF reserved for ISD with SCP02 without SCP80 s upport
diff --git a/pySim/gsm_r.py b/pySim/gsm_r.py
index 389a8cb..14dbb2d 100644
--- a/pySim/gsm_r.py
+++ b/pySim/gsm_r.py
@@ -1,13 +1,10 @@
-# -*- coding: utf-8 -*-
-
-# without this, pylint will fail when inner classes are used
-# within the 'nested' kwarg of our TlvMeta metaclass on python 3.7 :(
-# pylint: disable=undefined-variable
-
"""
The File (and its derived classes) uses the classes of pySim.filesystem in
order to describe the files specified in UIC Reference P38 T 9001 5.0 "FFFIS for GSM-R SIM Cards"
"""
+# without this, pylint will fail when inner classes are used
+# within the 'nested' kwarg of our TlvMeta metaclass on python 3.7 :(
+# pylint: disable=undefined-variable
#
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
@@ -28,16 +25,13 @@ order to describe the files specified in UIC Reference P38 T 9001 5.0 "FFFIS for
from pySim.utils import *
-#from pySim.tlv import *
from struct import pack, unpack
-from construct import *
+from construct import Struct, Bytes, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
from construct import Optional as COptional
-from pySim.construct import *
-import enum
+from pySim.construct import *
+from pySim.profile import CardProfileAddon
from pySim.filesystem import *
-import pySim.ts_102_221
-import pySim.ts_51_011
######################################################################
# DF.EIRENE (FFFIS for GSM-R SIM Cards)
@@ -77,15 +71,15 @@ class PlConfAdapter(Adapter):
num = int(obj) & 0x7
if num == 0:
return 'None'
- elif num == 1:
+ if num == 1:
return 4
- elif num == 2:
+ if num == 2:
return 3
- elif num == 3:
+ if num == 3:
return 2
- elif num == 4:
+ if num == 4:
return 1
- elif num == 5:
+ if num == 5:
return 0
def _encode(self, obj, context, path):
@@ -94,13 +88,13 @@ class PlConfAdapter(Adapter):
obj = int(obj)
if obj == 4:
return 1
- elif obj == 3:
+ if obj == 3:
return 2
- elif obj == 2:
+ if obj == 2:
return 3
- elif obj == 1:
+ if obj == 1:
return 4
- elif obj == 0:
+ if obj == 0:
return 5
@@ -111,19 +105,19 @@ class PlCallAdapter(Adapter):
num = int(obj) & 0x7
if num == 0:
return 'None'
- elif num == 1:
+ if num == 1:
return 4
- elif num == 2:
+ if num == 2:
return 3
- elif num == 3:
+ if num == 3:
return 2
- elif num == 4:
+ if num == 4:
return 1
- elif num == 5:
+ if num == 5:
return 0
- elif num == 6:
+ if num == 6:
return 'B'
- elif num == 7:
+ if num == 7:
return 'A'
def _encode(self, obj, context, path):
@@ -131,17 +125,17 @@ class PlCallAdapter(Adapter):
return 0
if obj == 4:
return 1
- elif obj == 3:
+ if obj == 3:
return 2
- elif obj == 2:
+ if obj == 2:
return 3
- elif obj == 1:
+ if obj == 1:
return 4
- elif obj == 0:
+ if obj == 0:
return 5
- elif obj == 'B':
+ if obj == 'B':
return 6
- elif obj == 'A':
+ if obj == 'A':
return 7
@@ -202,12 +196,12 @@ class EF_Shunting(TransparentEF):
class EF_GsmrPLMN(LinFixedEF):
"""Section 7.7"""
_test_de_encode = [
- ( "22f860f86f8d6f8e01", { "plmn": "228f06", "class_of_network": {
+ ( "22f860f86f8d6f8e01", { "plmn": "228-06", "class_of_network": {
"supported": { "vbs": True, "vgcs": True, "emlpp": True,
"fn": True, "eirene": True }, "preference": 0 },
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
"ic_table_ref": "01" } ),
- ( "22f810416f8d6f8e02", { "plmn": "228f01", "class_of_network": {
+ ( "22f810416f8d6f8e02", { "plmn": "228-01", "class_of_network": {
"supported": { "vbs": False, "vgcs": False, "emlpp": False,
"fn": True, "eirene": False }, "preference": 1 },
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
@@ -216,7 +210,7 @@ class EF_GsmrPLMN(LinFixedEF):
def __init__(self):
super().__init__(fid='6ff5', sfid=None, name='EF.GsmrPLMN',
desc='GSM-R network selection', rec_len=(9, 9))
- self._construct = Struct('plmn'/BcdAdapter(Bytes(3)),
+ self._construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
'class_of_network'/BitStruct('supported'/FlagsEnum(BitsInteger(5), vbs=1, vgcs=2, emlpp=4, fn=8, eirene=16),
'preference'/BitsInteger(3)),
'ic_incoming_ref_tbl'/HexAdapter(Bytes(2)),
@@ -362,3 +356,15 @@ class DF_EIRENE(CardDF):
desc='Free Number Call Type 0 and 8'),
]
self.add_files(files)
+
+
+class AddonGSMR(CardProfileAddon):
+ """An Addon that can be found on either classic GSM SIM or on UICC to support GSM-R."""
+ def __init__(self):
+ files = [
+ DF_EIRENE()
+ ]
+ super().__init__('GSM-R', desc='Railway GSM', files_in_mf=files)
+
+ def probe(self, card: 'CardBase') -> bool:
+ return card.file_exists(self.files_in_mf[0].fid)
diff --git a/pySim/gsmtap.py b/pySim/gsmtap.py
index 48a7af7..f113aeb 100644
--- a/pySim/gsmtap.py
+++ b/pySim/gsmtap.py
@@ -23,9 +23,9 @@ telecom-related protocol traces over UDP.
#
import socket
-from typing import List, Dict, Optional
from construct import Optional as COptional
-from construct import *
+from construct import Int8ub, Int8sb, Int32ub, BitStruct, Enum, GreedyBytes, Struct, Switch
+from construct import this, PaddedString
from pySim.construct import *
# The root definition of GSMTAP can be found at
diff --git a/pySim/iso7816_4.py b/pySim/iso7816_4.py
index 1da5809..64d814d 100644
--- a/pySim/iso7816_4.py
+++ b/pySim/iso7816_4.py
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-from construct import *
+from construct import GreedyBytes, GreedyString
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
diff --git a/pySim/jsonpath.py b/pySim/jsonpath.py
index 4dd838c..5778de9 100644
--- a/pySim/jsonpath.py
+++ b/pySim/jsonpath.py
@@ -1,8 +1,3 @@
-# coding=utf-8
-import json
-import pprint
-import jsonpath_ng
-
"""JSONpath utility functions as needed within pysim.
As pySim-sell has the ability to represent SIM files as JSON strings,
@@ -10,6 +5,8 @@ adding JSONpath allows us to conveniently modify individual sub-fields
of a file or record in its JSON representation.
"""
+import jsonpath_ng
+
# (C) 2021 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
diff --git a/pySim/legacy/__init__.py b/pySim/legacy/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pySim/legacy/__init__.py
diff --git a/pySim/legacy/cards.py b/pySim/legacy/cards.py
new file mode 100644
index 0000000..5e64e95
--- /dev/null
+++ b/pySim/legacy/cards.py
@@ -0,0 +1,1664 @@
+################################################################################
+# LEGACY
+################################################################################
+
+import abc
+from smartcard.util import toBytes
+from pytlv.TLV import *
+
+from pySim.cards import SimCardBase, UiccCardBase
+from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi, dec_msisdn, enc_msisdn
+from pySim.utils import enc_plmn, get_addr_type
+from pySim.utils import is_hex, h2b, b2h, h2s, s2h, lpad, rpad
+from pySim.legacy.utils import enc_ePDGSelection, format_xplmn_w_act, format_xplmn, dec_st, enc_st
+from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv
+from pySim.legacy.ts_51_011 import EF, DF
+from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
+from pySim.legacy.ts_31_103 import EF_ISIM_ADF_map
+
+from pySim.ts_51_011 import EF_AD, EF_SPN
+
+def format_addr(addr: str, addr_type: str) -> str:
+ """
+ helper function to format an FQDN (addr_type = '00') or IPv4
+ (addr_type = '01') address string into a printable string that
+ contains the hexadecimal representation and the original address
+ string (addr)
+ """
+ res = ""
+ if addr_type == '00': # FQDN
+ res += "\t%s # %s\n" % (s2h(addr), addr)
+ elif addr_type == '01': # IPv4
+ octets = addr.split(".")
+ addr_hex = ""
+ for o in octets:
+ addr_hex += ("%02x" % int(o))
+ res += "\t%s # %s\n" % (addr_hex, addr)
+ return res
+
+
+
+class SimCard(SimCardBase):
+ """Higher-layer class that is used *only* by legacy pySim-{prog,read}."""
+
+ def __init__(self, scc):
+ self._adm_chv_num = 4
+ super().__init__(scc)
+
+ def read_binary(self, ef, length=None, offset=0):
+ ef_path = ef in EF and EF[ef] or ef
+ return self._scc.read_binary(ef_path, length, offset)
+
+ def read_record(self, ef, rec_no):
+ ef_path = ef in EF and EF[ef] or ef
+ return self._scc.read_record(ef_path, rec_no)
+
+ def verify_adm(self, key):
+ """Authenticate with ADM key"""
+ (res, sw) = self._scc.verify_chv(self._adm_chv_num, key)
+ return sw
+
+ def read_iccid(self):
+ (res, sw) = self._scc.read_binary(EF['ICCID'])
+ if sw == '9000':
+ return (dec_iccid(res), sw)
+ else:
+ return (None, sw)
+
+ def update_iccid(self, iccid):
+ data, sw = self._scc.update_binary(EF['ICCID'], enc_iccid(iccid))
+ return sw
+
+ def read_imsi(self):
+ (res, sw) = self._scc.read_binary(EF['IMSI'])
+ if sw == '9000':
+ return (dec_imsi(res), sw)
+ else:
+ return (None, sw)
+
+ def update_imsi(self, imsi):
+ data, sw = self._scc.update_binary(EF['IMSI'], enc_imsi(imsi))
+ return sw
+
+ def update_acc(self, acc):
+ data, sw = self._scc.update_binary(EF['ACC'], lpad(acc, 4, c='0'))
+ return sw
+
+ def read_hplmn_act(self):
+ (res, sw) = self._scc.read_binary(EF['HPLMNAcT'])
+ if sw == '9000':
+ return (format_xplmn_w_act(res), sw)
+ else:
+ return (None, sw)
+
+ def update_hplmn_act(self, mcc, mnc, access_tech='FFFF'):
+ """
+ Update Home PLMN with access technology bit-field
+
+ See Section "10.3.37 EFHPLMNwAcT (HPLMN Selector with Access Technology)"
+ in ETSI TS 151 011 for the details of the access_tech field coding.
+ Some common values:
+ access_tech = '0080' # Only GSM is selected
+ access_tech = 'FFFF' # All technologies selected, even Reserved for Future Use ones
+ """
+ # get size and write EF.HPLMNwAcT
+ data = self._scc.read_binary(EF['HPLMNwAcT'], length=None, offset=0)
+ size = len(data[0]) // 2
+ hplmn = enc_plmn(mcc, mnc)
+ content = hplmn + access_tech
+ data, sw = self._scc.update_binary(
+ EF['HPLMNwAcT'], content + 'ffffff0000' * (size // 5 - 1))
+ return sw
+
+ def read_oplmn_act(self):
+ (res, sw) = self._scc.read_binary(EF['OPLMNwAcT'])
+ if sw == '9000':
+ return (format_xplmn_w_act(res), sw)
+ else:
+ return (None, sw)
+
+ def update_oplmn_act(self, mcc, mnc, access_tech='FFFF'):
+ """get size and write EF.OPLMNwAcT, See note in update_hplmn_act()"""
+ data = self._scc.read_binary(EF['OPLMNwAcT'], length=None, offset=0)
+ size = len(data[0]) // 2
+ hplmn = enc_plmn(mcc, mnc)
+ content = hplmn + access_tech
+ data, sw = self._scc.update_binary(
+ EF['OPLMNwAcT'], content + 'ffffff0000' * (size // 5 - 1))
+ return sw
+
+ def read_plmn_act(self):
+ (res, sw) = self._scc.read_binary(EF['PLMNwAcT'])
+ if sw == '9000':
+ return (format_xplmn_w_act(res), sw)
+ else:
+ return (None, sw)
+
+ def update_plmn_act(self, mcc, mnc, access_tech='FFFF'):
+ """get size and write EF.PLMNwAcT, See note in update_hplmn_act()"""
+ data = self._scc.read_binary(EF['PLMNwAcT'], length=None, offset=0)
+ size = len(data[0]) // 2
+ hplmn = enc_plmn(mcc, mnc)
+ content = hplmn + access_tech
+ data, sw = self._scc.update_binary(
+ EF['PLMNwAcT'], content + 'ffffff0000' * (size // 5 - 1))
+ return sw
+
+ def update_plmnsel(self, mcc, mnc):
+ data = self._scc.read_binary(EF['PLMNsel'], length=None, offset=0)
+ size = len(data[0]) // 2
+ hplmn = enc_plmn(mcc, mnc)
+ data, sw = self._scc.update_binary(
+ EF['PLMNsel'], hplmn + 'ff' * (size-3))
+ return sw
+
+ def update_smsp(self, smsp):
+ data, sw = self._scc.update_record(EF['SMSP'], 1, rpad(smsp, 84))
+ return sw
+
+ def update_ad(self, mnc=None, opmode=None, ofm=None, path=EF['AD']):
+ """
+ Update Administrative Data (AD)
+
+ See Sec. "4.2.18 EF_AD (Administrative Data)"
+ in 3GPP TS 31.102 for the details of the EF_AD contents.
+
+ Set any parameter to None to keep old value(s) on card.
+
+ Parameters:
+ mnc (str): MNC of IMSI
+ opmode (Hex-str, 1 Byte): MS Operation Mode
+ ofm (Hex-str, 1 Byte): Operational Feature Monitor (OFM) aka Ciphering Indicator
+ path (optional list with file path e.g. ['3f00', '7f20', '6fad'])
+
+ Returns:
+ str: Return code of write operation
+ """
+
+ ad = EF_AD()
+
+ # read from card
+ raw_hex_data, sw = self._scc.read_binary(
+ path, length=None, offset=0)
+ abstract_data = ad.decode_hex(raw_hex_data)
+
+ # perform updates
+ if mnc and abstract_data['extensions']:
+ # Note: Since we derive the length of the MNC by the string length
+ # of the mnc parameter, the caller must ensure that mnc has the
+ # correct length and is padded with zeros (if necessary).
+ mnclen = len(str(mnc))
+ if mnclen > 3 or mnclen < 2:
+ raise RuntimeError('invalid length of mnc "{}", expecting 2 or 3 digits'.format(mnc))
+ abstract_data['extensions']['mnc_len'] = mnclen
+ if opmode:
+ opmode_num = int(opmode, 16)
+ if opmode_num in [int(v) for v in EF_AD.OP_MODE]:
+ abstract_data['ms_operation_mode'] = opmode_num
+ else:
+ raise RuntimeError('invalid opmode "{}"'.format(opmode))
+ if ofm:
+ abstract_data['ofm'] = bool(int(ofm, 16))
+
+ # write to card
+ raw_hex_data = ad.encode_hex(abstract_data)
+ data, sw = self._scc.update_binary(path, raw_hex_data)
+ return sw
+
+ def read_spn(self):
+ (content, sw) = self._scc.read_binary(EF['SPN'])
+ if sw == '9000':
+ abstract_data = EF_SPN().decode_hex(content)
+ show_in_hplmn = abstract_data['show_in_hplmn']
+ hide_in_oplmn = abstract_data['hide_in_oplmn']
+ name = abstract_data['spn']
+ return ((name, show_in_hplmn, hide_in_oplmn), sw)
+ else:
+ return (None, sw)
+
+ def update_spn(self, name="", show_in_hplmn=False, hide_in_oplmn=False):
+ abstract_data = {
+ 'hide_in_oplmn': hide_in_oplmn,
+ 'show_in_hplmn': show_in_hplmn,
+ 'spn': name,
+ }
+ content = EF_SPN().encode_hex(abstract_data)
+ data, sw = self._scc.update_binary(EF['SPN'], content)
+ return sw
+
+ def read_gid1(self):
+ (res, sw) = self._scc.read_binary(EF['GID1'])
+ if sw == '9000':
+ return (res, sw)
+ else:
+ return (None, sw)
+
+ def read_msisdn(self):
+ (res, sw) = self._scc.read_record(EF['MSISDN'], 1)
+ if sw == '9000':
+ return (dec_msisdn(res), sw)
+ else:
+ return (None, sw)
+
+
+class UsimCard(UiccCardBase, SimCard):
+ """Higher-layer class that is used *only* by legacy pySim-{prog,read}."""
+
+ def read_ehplmn(self):
+ (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['EHPLMN'])
+ if sw == '9000':
+ return (format_xplmn(res), sw)
+ else:
+ return (None, sw)
+
+ def update_ehplmn(self, mcc, mnc):
+ data = self._scc.read_binary(
+ EF_USIM_ADF_map['EHPLMN'], length=None, offset=0)
+ size = len(data[0]) // 2
+ ehplmn = enc_plmn(mcc, mnc)
+ data, sw = self._scc.update_binary(EF_USIM_ADF_map['EHPLMN'], ehplmn)
+ return sw
+
+ def read_fplmn(self):
+ res, sw = self._scc.read_binary(EF_USIM_ADF_map['FPLMN'])
+ if sw == '9000':
+ return format_xplmn(res), sw
+ else:
+ return None, sw
+
+ def update_fplmn(self, fplmn):
+ self._scc.select_file('3f00')
+ self.select_adf_by_aid('USIM')
+ size = self._scc.binary_size(EF_USIM_ADF_map['FPLMN'])
+ encoded = ''.join([enc_plmn(plmn[:3], plmn[3:]) for plmn in fplmn])
+ encoded = rpad(encoded, size)
+ data, sw = self._scc.update_binary(EF_USIM_ADF_map['FPLMN'], encoded)
+ return sw
+
+ def read_epdgid(self):
+ (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['ePDGId'])
+ if sw == '9000':
+ try:
+ addr, addr_type = dec_addr_tlv(res)
+ except:
+ addr = None
+ addr_type = None
+ return (format_addr(addr, addr_type), sw)
+ else:
+ return (None, sw)
+
+ def update_epdgid(self, epdgid):
+ size = self._scc.binary_size(EF_USIM_ADF_map['ePDGId']) * 2
+ if len(epdgid) > 0:
+ addr_type = get_addr_type(epdgid)
+ if addr_type == None:
+ raise ValueError(
+ "Unknown ePDG Id address type or invalid address provided")
+ epdgid_tlv = rpad(enc_addr_tlv(epdgid, ('%02x' % addr_type)), size)
+ else:
+ epdgid_tlv = rpad('ff', size)
+ data, sw = self._scc.update_binary(
+ EF_USIM_ADF_map['ePDGId'], epdgid_tlv)
+ return sw
+
+ def read_ePDGSelection(self):
+ (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['ePDGSelection'])
+ if sw == '9000':
+ return (format_ePDGSelection(res), sw)
+ else:
+ return (None, sw)
+
+ def update_ePDGSelection(self, mcc, mnc):
+ (res, sw) = self._scc.read_binary(
+ EF_USIM_ADF_map['ePDGSelection'], length=None, offset=0)
+ if sw == '9000' and (len(mcc) == 0 or len(mnc) == 0):
+ # Reset contents
+ # 80 - Tag value
+ (res, sw) = self._scc.update_binary(
+ EF_USIM_ADF_map['ePDGSelection'], rpad('', len(res)))
+ elif sw == '9000':
+ (res, sw) = self._scc.update_binary(
+ EF_USIM_ADF_map['ePDGSelection'], enc_ePDGSelection(res, mcc, mnc))
+ return sw
+
+ def read_ust(self):
+ (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['UST'])
+ if sw == '9000':
+ # Print those which are available
+ return ([res, dec_st(res, table="usim")], sw)
+ else:
+ return ([None, None], sw)
+
+ def update_ust(self, service, bit=1):
+ (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['UST'])
+ if sw == '9000':
+ content = enc_st(res, service, bit)
+ (res, sw) = self._scc.update_binary(
+ EF_USIM_ADF_map['UST'], content)
+ return sw
+
+ def update_est(self, service, bit=1):
+ (res, sw) = self._scc.read_binary(EF_USIM_ADF_map['EST'])
+ if sw == '9000':
+ content = enc_st(res, service, bit)
+ (res, sw) = self._scc.update_binary(
+ EF_USIM_ADF_map['EST'], content)
+ return sw
+
+
+
+class IsimCard(UiccCardBase):
+ """Higher-layer class that is used *only* by legacy pySim-{prog,read}."""
+
+ name = 'ISIM'
+
+ def read_pcscf(self):
+ rec_cnt = self._scc.record_count(EF_ISIM_ADF_map['PCSCF'])
+ pcscf_recs = ""
+ for i in range(0, rec_cnt):
+ (res, sw) = self._scc.read_record(EF_ISIM_ADF_map['PCSCF'], i + 1)
+ if sw == '9000':
+ try:
+ addr, addr_type = dec_addr_tlv(res)
+ except:
+ addr = None
+ addr_type = None
+ content = format_addr(addr, addr_type)
+ pcscf_recs += "%s" % (len(content)
+ and content or '\tNot available\n')
+ else:
+ pcscf_recs += "\tP-CSCF: Can't read, response code = %s\n" % (
+ sw)
+ return pcscf_recs
+
+ def update_pcscf(self, pcscf):
+ if len(pcscf) > 0:
+ addr_type = get_addr_type(pcscf)
+ if addr_type == None:
+ raise ValueError(
+ "Unknown PCSCF address type or invalid address provided")
+ content = enc_addr_tlv(pcscf, ('%02x' % addr_type))
+ else:
+ # Just the tag value
+ content = '80'
+ rec_size_bytes = self._scc.record_size(EF_ISIM_ADF_map['PCSCF'])
+ pcscf_tlv = rpad(content, rec_size_bytes*2)
+ data, sw = self._scc.update_record(
+ EF_ISIM_ADF_map['PCSCF'], 1, pcscf_tlv)
+ return sw
+
+ def read_domain(self):
+ (res, sw) = self._scc.read_binary(EF_ISIM_ADF_map['DOMAIN'])
+ if sw == '9000':
+ # Skip the initial tag value ('80') byte and get length of contents
+ length = int(res[2:4], 16)
+ content = h2s(res[4:4+(length*2)])
+ return (content, sw)
+ else:
+ return (None, sw)
+
+ def update_domain(self, domain=None, mcc=None, mnc=None):
+ hex_str = ""
+ if domain:
+ hex_str = s2h(domain)
+ elif mcc and mnc:
+ # MCC and MNC always has 3 digits in domain form
+ plmn_str = 'mnc' + lpad(mnc, 3, "0") + '.mcc' + lpad(mcc, 3, "0")
+ hex_str = s2h('ims.' + plmn_str + '.3gppnetwork.org')
+
+ # Build TLV
+ tlv = TLV(['80'])
+ content = tlv.build({'80': hex_str})
+
+ bin_size_bytes = self._scc.binary_size(EF_ISIM_ADF_map['DOMAIN'])
+ data, sw = self._scc.update_binary(
+ EF_ISIM_ADF_map['DOMAIN'], rpad(content, bin_size_bytes*2))
+ return sw
+
+ def read_impi(self):
+ (res, sw) = self._scc.read_binary(EF_ISIM_ADF_map['IMPI'])
+ if sw == '9000':
+ # Skip the initial tag value ('80') byte and get length of contents
+ length = int(res[2:4], 16)
+ content = h2s(res[4:4+(length*2)])
+ return (content, sw)
+ else:
+ return (None, sw)
+
+ def update_impi(self, impi=None):
+ hex_str = ""
+ if impi:
+ hex_str = s2h(impi)
+ # Build TLV
+ tlv = TLV(['80'])
+ content = tlv.build({'80': hex_str})
+
+ bin_size_bytes = self._scc.binary_size(EF_ISIM_ADF_map['IMPI'])
+ data, sw = self._scc.update_binary(
+ EF_ISIM_ADF_map['IMPI'], rpad(content, bin_size_bytes*2))
+ return sw
+
+ def read_impu(self):
+ rec_cnt = self._scc.record_count(EF_ISIM_ADF_map['IMPU'])
+ impu_recs = ""
+ for i in range(0, rec_cnt):
+ (res, sw) = self._scc.read_record(EF_ISIM_ADF_map['IMPU'], i + 1)
+ if sw == '9000':
+ # Skip the initial tag value ('80') byte and get length of contents
+ length = int(res[2:4], 16)
+ content = h2s(res[4:4+(length*2)])
+ impu_recs += "\t%s\n" % (len(content)
+ and content or 'Not available')
+ else:
+ impu_recs += "IMS public user identity: Can't read, response code = %s\n" % (
+ sw)
+ return impu_recs
+
+ def update_impu(self, impu=None):
+ hex_str = ""
+ if impu:
+ hex_str = s2h(impu)
+ # Build TLV
+ tlv = TLV(['80'])
+ content = tlv.build({'80': hex_str})
+
+ rec_size_bytes = self._scc.record_size(EF_ISIM_ADF_map['IMPU'])
+ impu_tlv = rpad(content, rec_size_bytes*2)
+ data, sw = self._scc.update_record(
+ EF_ISIM_ADF_map['IMPU'], 1, impu_tlv)
+ return sw
+
+ def read_iari(self):
+ rec_cnt = self._scc.record_count(EF_ISIM_ADF_map['UICCIARI'])
+ uiari_recs = ""
+ for i in range(0, rec_cnt):
+ (res, sw) = self._scc.read_record(
+ EF_ISIM_ADF_map['UICCIARI'], i + 1)
+ if sw == '9000':
+ # Skip the initial tag value ('80') byte and get length of contents
+ length = int(res[2:4], 16)
+ content = h2s(res[4:4+(length*2)])
+ uiari_recs += "\t%s\n" % (len(content)
+ and content or 'Not available')
+ else:
+ uiari_recs += "UICC IARI: Can't read, response code = %s\n" % (
+ sw)
+ return uiari_recs
+
+ def update_ist(self, service, bit=1):
+ (res, sw) = self._scc.read_binary(EF_ISIM_ADF_map['IST'])
+ if sw == '9000':
+ content = enc_st(res, service, bit)
+ (res, sw) = self._scc.update_binary(
+ EF_ISIM_ADF_map['IST'], content)
+ return sw
+
+
+class MagicSimBase(abc.ABC, SimCard):
+ """
+ Theses cards uses several record based EFs to store the provider infos,
+ each possible provider uses a specific record number in each EF. The
+ indexes used are ( where N is the number of providers supported ) :
+ - [2 .. N+1] for the operator name
+ - [1 .. N] for the programmable EFs
+
+ * 3f00/7f4d/8f0c : Operator Name
+
+ bytes 0-15 : provider name, padded with 0xff
+ byte 16 : length of the provider name
+ byte 17 : 01 for valid records, 00 otherwise
+
+ * 3f00/7f4d/8f0d : Programmable Binary EFs
+
+ * 3f00/7f4d/8f0e : Programmable Record EFs
+
+ """
+
+ _files = {} # type: Dict[str, Tuple[str, int, bool]]
+ _ki_file = None # type: Optional[str]
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ for p, l, t in kls._files.values():
+ if not t:
+ continue
+ if scc.record_size(['3f00', '7f4d', p]) != l:
+ return None
+ except:
+ return None
+
+ return kls(scc)
+
+ def _get_count(self):
+ """
+ Selects the file and returns the total number of entries
+ and entry size
+ """
+ f = self._files['name']
+
+ r = self._scc.select_path(['3f00', '7f4d', f[0]])
+ rec_len = int(r[-1][28:30], 16)
+ tlen = int(r[-1][4:8], 16)
+ rec_cnt = (tlen // rec_len) - 1
+
+ if (rec_cnt < 1) or (rec_len != f[1]):
+ raise RuntimeError('Bad card type')
+
+ return rec_cnt
+
+ def program(self, p):
+ # Go to dir
+ self._scc.select_path(['3f00', '7f4d'])
+
+ # Home PLMN in PLMN_Sel format
+ hplmn = enc_plmn(p['mcc'], p['mnc'])
+
+ # Operator name ( 3f00/7f4d/8f0c )
+ self._scc.update_record(self._files['name'][0], 2,
+ rpad(b2h(p['name']), 32) + ('%02x' %
+ len(p['name'])) + '01'
+ )
+
+ # ICCID/IMSI/Ki/HPLMN ( 3f00/7f4d/8f0d )
+ v = ''
+
+ # inline Ki
+ if self._ki_file is None:
+ v += p['ki']
+
+ # ICCID
+ v += '3f00' + '2fe2' + '0a' + enc_iccid(p['iccid'])
+
+ # IMSI
+ v += '7f20' + '6f07' + '09' + enc_imsi(p['imsi'])
+
+ # Ki
+ if self._ki_file:
+ v += self._ki_file + '10' + p['ki']
+
+ # PLMN_Sel
+ v += '6f30' + '18' + rpad(hplmn, 36)
+
+ # ACC
+ # This doesn't work with "fake" SuperSIM cards,
+ # but will hopefully work with real SuperSIMs.
+ if p.get('acc') is not None:
+ v += '6f78' + '02' + lpad(p['acc'], 4)
+
+ self._scc.update_record(self._files['b_ef'][0], 1,
+ rpad(v, self._files['b_ef'][1]*2)
+ )
+
+ # SMSP ( 3f00/7f4d/8f0e )
+ # FIXME
+
+ # Write PLMN_Sel forcefully as well
+ r = self._scc.select_path(['3f00', '7f20', '6f30'])
+ tl = int(r[-1][4:8], 16)
+
+ hplmn = enc_plmn(p['mcc'], p['mnc'])
+ self._scc.update_binary('6f30', hplmn + 'ff' * (tl-3))
+
+ def erase(self):
+ # Dummy
+ df = {}
+ for k, v in self._files.items():
+ ofs = 1
+ fv = v[1] * 'ff'
+ if k == 'name':
+ ofs = 2
+ fv = fv[0:-4] + '0000'
+ df[v[0]] = (fv, ofs)
+
+ # Write
+ for n in range(0, self._get_count()):
+ for k, (msg, ofs) in df.items():
+ self._scc.update_record(['3f00', '7f4d', k], n + ofs, msg)
+
+
+class SuperSim(MagicSimBase):
+
+ name = 'supersim'
+
+ _files = {
+ 'name': ('8f0c', 18, True),
+ 'b_ef': ('8f0d', 74, True),
+ 'r_ef': ('8f0e', 50, True),
+ }
+
+ _ki_file = None
+
+
+class MagicSim(MagicSimBase):
+
+ name = 'magicsim'
+
+ _files = {
+ 'name': ('8f0c', 18, True),
+ 'b_ef': ('8f0d', 130, True),
+ 'r_ef': ('8f0e', 102, False),
+ }
+
+ _ki_file = '6f1b'
+
+
+class FakeMagicSim(SimCard):
+ """
+ Theses cards have a record based EF 3f00/000c that contains the provider
+ information. See the program method for its format. The records go from
+ 1 to N.
+ """
+
+ name = 'fakemagicsim'
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ if scc.record_size(['3f00', '000c']) != 0x5a:
+ return None
+ except:
+ return None
+
+ return kls(scc)
+
+ def _get_infos(self):
+ """
+ Selects the file and returns the total number of entries
+ and entry size
+ """
+
+ r = self._scc.select_path(['3f00', '000c'])
+ rec_len = int(r[-1][28:30], 16)
+ tlen = int(r[-1][4:8], 16)
+ rec_cnt = (tlen // rec_len) - 1
+
+ if (rec_cnt < 1) or (rec_len != 0x5a):
+ raise RuntimeError('Bad card type')
+
+ return rec_cnt, rec_len
+
+ def program(self, p):
+ # Home PLMN
+ r = self._scc.select_path(['3f00', '7f20', '6f30'])
+ tl = int(r[-1][4:8], 16)
+
+ hplmn = enc_plmn(p['mcc'], p['mnc'])
+ self._scc.update_binary('6f30', hplmn + 'ff' * (tl-3))
+
+ # Get total number of entries and entry size
+ rec_cnt, rec_len = self._get_infos()
+
+ # Set first entry
+ entry = (
+ '81' + # 1b Status: Valid & Active
+ rpad(s2h(p['name'][0:14]), 28) + # 14b Entry Name
+ enc_iccid(p['iccid']) + # 10b ICCID
+ enc_imsi(p['imsi']) + # 9b IMSI_len + id_type(9) + IMSI
+ p['ki'] + # 16b Ki
+ lpad(p['smsp'], 80) # 40b SMSP (padded with ff if needed)
+ )
+ self._scc.update_record('000c', 1, entry)
+
+ def erase(self):
+ # Get total number of entries and entry size
+ rec_cnt, rec_len = self._get_infos()
+
+ # Erase all entries
+ entry = 'ff' * rec_len
+ for i in range(0, rec_cnt):
+ self._scc.update_record('000c', 1+i, entry)
+
+
+class GrcardSim(SimCard):
+ """
+ Greencard (grcard.cn) HZCOS GSM SIM
+ These cards have a much more regular ISO 7816-4 / TS 11.11 structure,
+ and use standard UPDATE RECORD / UPDATE BINARY commands except for Ki.
+ """
+
+ name = 'grcardsim'
+
+ @classmethod
+ def autodetect(kls, scc):
+ return None
+
+ def program(self, p):
+ # We don't really know yet what ADM PIN 4 is about
+ #self._scc.verify_chv(4, h2b("4444444444444444"))
+
+ # Authenticate using ADM PIN 5
+ if p['pin_adm']:
+ pin = h2b(p['pin_adm'])
+ else:
+ pin = h2b("4444444444444444")
+ self._scc.verify_chv(5, pin)
+
+ # EF.ICCID
+ r = self._scc.select_path(['3f00', '2fe2'])
+ data, sw = self._scc.update_binary('2fe2', enc_iccid(p['iccid']))
+
+ # EF.IMSI
+ r = self._scc.select_path(['3f00', '7f20', '6f07'])
+ data, sw = self._scc.update_binary('6f07', enc_imsi(p['imsi']))
+
+ # EF.ACC
+ if p.get('acc') is not None:
+ data, sw = self._scc.update_binary('6f78', lpad(p['acc'], 4))
+
+ # EF.SMSP
+ if p.get('smsp'):
+ r = self._scc.select_path(['3f00', '7f10', '6f42'])
+ data, sw = self._scc.update_record('6f42', 1, lpad(p['smsp'], 80))
+
+ # Set the Ki using proprietary command
+ pdu = '80d4020010' + p['ki']
+ data, sw = self._scc.send_apdu(pdu)
+
+ # EF.HPLMN
+ r = self._scc.select_path(['3f00', '7f20', '6f30'])
+ size = int(r[-1][4:8], 16)
+ hplmn = enc_plmn(p['mcc'], p['mnc'])
+ self._scc.update_binary('6f30', hplmn + 'ff' * (size-3))
+
+ # EF.SPN (Service Provider Name)
+ r = self._scc.select_path(['3f00', '7f20', '6f30'])
+ size = int(r[-1][4:8], 16)
+ # FIXME
+
+ # FIXME: EF.MSISDN
+
+
+class SysmoSIMgr1(GrcardSim):
+ """
+ sysmocom sysmoSIM-GR1
+ These cards have a much more regular ISO 7816-4 / TS 11.11 structure,
+ and use standard UPDATE RECORD / UPDATE BINARY commands except for Ki.
+ """
+ name = 'sysmosim-gr1'
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ # Look for ATR
+ if scc.get_atr() == toBytes("3B 99 18 00 11 88 22 33 44 55 66 77 60"):
+ return kls(scc)
+ except:
+ return None
+ return None
+
+
+class SysmoUSIMgr1(UsimCard):
+ """
+ sysmocom sysmoUSIM-GR1
+ """
+ name = 'sysmoUSIM-GR1'
+
+ @classmethod
+ def autodetect(kls, scc):
+ # TODO: Access the ATR
+ return None
+
+ def program(self, p):
+ # TODO: check if verify_chv could be used or what it needs
+ # self._scc.verify_chv(0x0A, [0x33,0x32,0x32,0x31,0x33,0x32,0x33,0x32])
+ # Unlock the card..
+ data, sw = self._scc.send_apdu_checksw(
+ "0020000A083332323133323332")
+
+ # TODO: move into SimCardCommands
+ par = (p['ki'] + # 16b K
+ p['opc'] + # 32b OPC
+ enc_iccid(p['iccid']) + # 10b ICCID
+ enc_imsi(p['imsi']) # 9b IMSI_len + id_type(9) + IMSI
+ )
+ data, sw = self._scc.send_apdu_checksw("0099000033" + par)
+
+
+class SysmoSIMgr2(SimCard):
+ """
+ sysmocom sysmoSIM-GR2
+ """
+
+ name = 'sysmoSIM-GR2'
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ # Look for ATR
+ if scc.get_atr() == toBytes("3B 7D 94 00 00 55 55 53 0A 74 86 93 0B 24 7C 4D 54 68"):
+ return kls(scc)
+ except:
+ return None
+ return None
+
+ def program(self, p):
+
+ # select MF
+ r = self._scc.select_path(['3f00'])
+
+ # authenticate as SUPER ADM using default key
+ self._scc.verify_chv(0x0b, h2b("3838383838383838"))
+
+ # set ADM pin using proprietary command
+ # INS: D4
+ # P1: 3A for PIN, 3B for PUK
+ # P2: CHV number, as in VERIFY CHV for PIN, and as in UNBLOCK CHV for PUK
+ # P3: 08, CHV length (curiously the PUK is also 08 length, instead of 10)
+ if p['pin_adm']:
+ pin = h2b(p['pin_adm'])
+ else:
+ pin = h2b("4444444444444444")
+
+ pdu = 'A0D43A0508' + b2h(pin)
+ data, sw = self._scc.send_apdu(pdu)
+
+ # authenticate as ADM (enough to write file, and can set PINs)
+
+ self._scc.verify_chv(0x05, pin)
+
+ # write EF.ICCID
+ data, sw = self._scc.update_binary('2fe2', enc_iccid(p['iccid']))
+
+ # select DF_GSM
+ r = self._scc.select_path(['7f20'])
+
+ # write EF.IMSI
+ data, sw = self._scc.update_binary('6f07', enc_imsi(p['imsi']))
+
+ # write EF.ACC
+ if p.get('acc') is not None:
+ data, sw = self._scc.update_binary('6f78', lpad(p['acc'], 4))
+
+ # get size and write EF.HPLMN
+ r = self._scc.select_path(['6f30'])
+ size = int(r[-1][4:8], 16)
+ hplmn = enc_plmn(p['mcc'], p['mnc'])
+ self._scc.update_binary('6f30', hplmn + 'ff' * (size-3))
+
+ # set COMP128 version 0 in proprietary file
+ data, sw = self._scc.update_binary('0001', '001000')
+
+ # set Ki in proprietary file
+ data, sw = self._scc.update_binary('0001', p['ki'], 3)
+
+ # select DF_TELECOM
+ r = self._scc.select_path(['3f00', '7f10'])
+
+ # write EF.SMSP
+ if p.get('smsp'):
+ data, sw = self._scc.update_record('6f42', 1, lpad(p['smsp'], 80))
+
+
+class SysmoUSIMSJS1(UsimCard):
+ """
+ sysmocom sysmoUSIM-SJS1
+ """
+
+ name = 'sysmoUSIM-SJS1'
+
+ def __init__(self, ssc):
+ super(SysmoUSIMSJS1, self).__init__(ssc)
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ # Look for ATR
+ if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 43 20 07 18 00 00 01 A5"):
+ return kls(scc)
+ except:
+ return None
+ return None
+
+ def verify_adm(self, key):
+ # authenticate as ADM using default key (written on the card..)
+ if not key:
+ raise ValueError(
+ "Please provide a PIN-ADM as there is no default one")
+ (res, sw) = self._scc.verify_chv(0x0A, key)
+ return sw
+
+ def program(self, p):
+ self.verify_adm(h2b(p['pin_adm']))
+
+ # select MF
+ r = self._scc.select_path(['3f00'])
+
+ # write EF.ICCID
+ data, sw = self._scc.update_binary('2fe2', enc_iccid(p['iccid']))
+
+ # select DF_GSM
+ r = self._scc.select_path(['7f20'])
+
+ # set Ki in proprietary file
+ data, sw = self._scc.update_binary('00FF', p['ki'])
+
+ # set OPc in proprietary file
+ if 'opc' in p:
+ content = "01" + p['opc']
+ data, sw = self._scc.update_binary('00F7', content)
+
+ # set Service Provider Name
+ if p.get('name') is not None:
+ self.update_spn(p['name'], True, True)
+
+ if p.get('acc') is not None:
+ self.update_acc(p['acc'])
+
+ # write EF.IMSI
+ data, sw = self._scc.update_binary('6f07', enc_imsi(p['imsi']))
+
+ # EF.PLMNsel
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_plmnsel(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming PLMNsel failed with code %s" % sw)
+
+ # EF.PLMNwAcT
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_plmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming PLMNwAcT failed with code %s" % sw)
+
+ # EF.OPLMNwAcT
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_oplmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming OPLMNwAcT failed with code %s" % sw)
+
+ # EF.HPLMNwAcT
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_hplmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming HPLMNwAcT failed with code %s" % sw)
+
+ # EF.AD
+ if (p.get('mcc') and p.get('mnc')) or p.get('opmode'):
+ if p.get('mcc') and p.get('mnc'):
+ mnc = p['mnc']
+ else:
+ mnc = None
+ sw = self.update_ad(mnc=mnc, opmode=p.get('opmode'))
+ if sw != '9000':
+ print("Programming AD failed with code %s" % sw)
+
+ # EF.SMSP
+ if p.get('smsp'):
+ r = self._scc.select_path(['3f00', '7f10'])
+ data, sw = self._scc.update_record(
+ '6f42', 1, lpad(p['smsp'], 104), force_len=True)
+
+ # EF.MSISDN
+ # TODO: Alpha Identifier (currently 'ff'O * 20)
+ # TODO: Capability/Configuration1 Record Identifier
+ # TODO: Extension1 Record Identifier
+ if p.get('msisdn') is not None:
+ msisdn = enc_msisdn(p['msisdn'])
+ data = 'ff' * 20 + msisdn
+
+ r = self._scc.select_path(['3f00', '7f10'])
+ data, sw = self._scc.update_record('6F40', 1, data, force_len=True)
+
+
+class FairwavesSIM(UsimCard):
+ """
+ FairwavesSIM
+
+ The SIM card is operating according to the standard.
+ For Ki/OP/OPC programming the following files are additionally open for writing:
+ 3F00/7F20/FF01 – OP/OPC:
+ byte 1 = 0x01, bytes 2-17: OPC;
+ byte 1 = 0x00, bytes 2-17: OP;
+ 3F00/7F20/FF02: Ki
+ """
+
+ name = 'Fairwaves-SIM'
+ # Propriatary files
+ _EF_num = {
+ 'Ki': 'FF02',
+ 'OP/OPC': 'FF01',
+ }
+ _EF = {
+ 'Ki': DF['GSM']+[_EF_num['Ki']],
+ 'OP/OPC': DF['GSM']+[_EF_num['OP/OPC']],
+ }
+
+ def __init__(self, ssc):
+ super(FairwavesSIM, self).__init__(ssc)
+ self._adm_chv_num = 0x11
+ self._adm2_chv_num = 0x12
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ # Look for ATR
+ if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 44 22 06 10 00 00 01 A9"):
+ return kls(scc)
+ except:
+ return None
+ return None
+
+ def verify_adm2(self, key):
+ '''
+ Authenticate with ADM2 key.
+
+ Fairwaves SIM cards support hierarchical key structure and ADM2 key
+ is a key which has access to proprietary files (Ki and OP/OPC).
+ That said, ADM key inherits permissions of ADM2 key and thus we rarely
+ need ADM2 key per se.
+ '''
+ (res, sw) = self._scc.verify_chv(self._adm2_chv_num, key)
+ return sw
+
+ def read_ki(self):
+ """
+ Read Ki in proprietary file.
+
+ Requires ADM1 access level
+ """
+ return self._scc.read_binary(self._EF['Ki'])
+
+ def update_ki(self, ki):
+ """
+ Set Ki in proprietary file.
+
+ Requires ADM1 access level
+ """
+ data, sw = self._scc.update_binary(self._EF['Ki'], ki)
+ return sw
+
+ def read_op_opc(self):
+ """
+ Read Ki in proprietary file.
+
+ Requires ADM1 access level
+ """
+ (ef, sw) = self._scc.read_binary(self._EF['OP/OPC'])
+ type = 'OP' if ef[0:2] == '00' else 'OPC'
+ return ((type, ef[2:]), sw)
+
+ def update_op(self, op):
+ """
+ Set OP in proprietary file.
+
+ Requires ADM1 access level
+ """
+ content = '00' + op
+ data, sw = self._scc.update_binary(self._EF['OP/OPC'], content)
+ return sw
+
+ def update_opc(self, opc):
+ """
+ Set OPC in proprietary file.
+
+ Requires ADM1 access level
+ """
+ content = '01' + opc
+ data, sw = self._scc.update_binary(self._EF['OP/OPC'], content)
+ return sw
+
+ def program(self, p):
+ # For some reason the card programming only works when the card
+ # is handled as a classic SIM, even though it is an USIM, so we
+ # reconfigure the class byte and the select control field on
+ # the fly. When the programming is done the original values are
+ # restored.
+ cla_byte_orig = self._scc.cla_byte
+ sel_ctrl_orig = self._scc.sel_ctrl
+ self._scc.cla_byte = "a0"
+ self._scc.sel_ctrl = "0000"
+
+ try:
+ self._program(p)
+ finally:
+ # restore original cla byte and sel ctrl
+ self._scc.cla_byte = cla_byte_orig
+ self._scc.sel_ctrl = sel_ctrl_orig
+
+ def _program(self, p):
+ # authenticate as ADM1
+ if not p['pin_adm']:
+ raise ValueError(
+ "Please provide a PIN-ADM as there is no default one")
+ self.verify_adm(h2b(p['pin_adm']))
+
+ # TODO: Set operator name
+ if p.get('smsp') is not None:
+ sw = self.update_smsp(p['smsp'])
+ if sw != '9000':
+ print("Programming SMSP failed with code %s" % sw)
+ # This SIM doesn't support changing ICCID
+ if p.get('mcc') is not None and p.get('mnc') is not None:
+ sw = self.update_hplmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming MCC/MNC failed with code %s" % sw)
+ if p.get('imsi') is not None:
+ sw = self.update_imsi(p['imsi'])
+ if sw != '9000':
+ print("Programming IMSI failed with code %s" % sw)
+ if p.get('ki') is not None:
+ sw = self.update_ki(p['ki'])
+ if sw != '9000':
+ print("Programming Ki failed with code %s" % sw)
+ if p.get('opc') is not None:
+ sw = self.update_opc(p['opc'])
+ if sw != '9000':
+ print("Programming OPC failed with code %s" % sw)
+ if p.get('acc') is not None:
+ sw = self.update_acc(p['acc'])
+ if sw != '9000':
+ print("Programming ACC failed with code %s" % sw)
+
+
+class OpenCellsSim(SimCard):
+ """
+ OpenCellsSim
+
+ """
+
+ name = 'OpenCells-SIM'
+
+ def __init__(self, ssc):
+ super(OpenCellsSim, self).__init__(ssc)
+ self._adm_chv_num = 0x0A
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ # Look for ATR
+ if scc.get_atr() == toBytes("3B 9F 95 80 1F C3 80 31 E0 73 FE 21 13 57 86 81 02 86 98 44 18 A8"):
+ return kls(scc)
+ except:
+ return None
+ return None
+
+ def program(self, p):
+ if not p['pin_adm']:
+ raise ValueError(
+ "Please provide a PIN-ADM as there is no default one")
+ self._scc.verify_chv(0x0A, h2b(p['pin_adm']))
+
+ # select MF
+ r = self._scc.select_path(['3f00'])
+
+ # write EF.ICCID
+ data, sw = self._scc.update_binary('2fe2', enc_iccid(p['iccid']))
+
+ r = self._scc.select_path(['7ff0'])
+
+ # set Ki in proprietary file
+ data, sw = self._scc.update_binary('FF02', p['ki'])
+
+ # set OPC in proprietary file
+ data, sw = self._scc.update_binary('FF01', p['opc'])
+
+ # select DF_GSM
+ r = self._scc.select_path(['7f20'])
+
+ # write EF.IMSI
+ data, sw = self._scc.update_binary('6f07', enc_imsi(p['imsi']))
+
+
+class WavemobileSim(UsimCard):
+ """
+ WavemobileSim
+
+ """
+
+ name = 'Wavemobile-SIM'
+
+ def __init__(self, ssc):
+ super(WavemobileSim, self).__init__(ssc)
+ self._adm_chv_num = 0x0A
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ # Look for ATR
+ if scc.get_atr() == toBytes("3B 9F 95 80 1F C7 80 31 E0 73 F6 21 13 67 4D 45 16 00 43 01 00 8F"):
+ return kls(scc)
+ except:
+ return None
+ return None
+
+ def program(self, p):
+ if not p['pin_adm']:
+ raise ValueError(
+ "Please provide a PIN-ADM as there is no default one")
+ self.verify_adm(h2b(p['pin_adm']))
+
+ # EF.ICCID
+ # TODO: Add programming of the ICCID
+ if p.get('iccid'):
+ print(
+ "Warning: Programming of the ICCID is not implemented for this type of card.")
+
+ # KI (Presumably a proprietary file)
+ # TODO: Add programming of KI
+ if p.get('ki'):
+ print(
+ "Warning: Programming of the KI is not implemented for this type of card.")
+
+ # OPc (Presumably a proprietary file)
+ # TODO: Add programming of OPc
+ if p.get('opc'):
+ print(
+ "Warning: Programming of the OPc is not implemented for this type of card.")
+
+ # EF.SMSP
+ if p.get('smsp'):
+ sw = self.update_smsp(p['smsp'])
+ if sw != '9000':
+ print("Programming SMSP failed with code %s" % sw)
+
+ # EF.IMSI
+ if p.get('imsi'):
+ sw = self.update_imsi(p['imsi'])
+ if sw != '9000':
+ print("Programming IMSI failed with code %s" % sw)
+
+ # EF.ACC
+ if p.get('acc'):
+ sw = self.update_acc(p['acc'])
+ if sw != '9000':
+ print("Programming ACC failed with code %s" % sw)
+
+ # EF.PLMNsel
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_plmnsel(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming PLMNsel failed with code %s" % sw)
+
+ # EF.PLMNwAcT
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_plmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming PLMNwAcT failed with code %s" % sw)
+
+ # EF.OPLMNwAcT
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_oplmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming OPLMNwAcT failed with code %s" % sw)
+
+ # EF.AD
+ if (p.get('mcc') and p.get('mnc')) or p.get('opmode'):
+ if p.get('mcc') and p.get('mnc'):
+ mnc = p['mnc']
+ else:
+ mnc = None
+ sw = self.update_ad(mnc=mnc, opmode=p.get('opmode'))
+ if sw != '9000':
+ print("Programming AD failed with code %s" % sw)
+
+ return None
+
+
+class SysmoISIMSJA2(UsimCard, IsimCard):
+ """
+ sysmocom sysmoISIM-SJA2
+ """
+
+ name = 'sysmoISIM-SJA2'
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ # Try card model #1
+ atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9"
+ if scc.get_atr() == toBytes(atr):
+ return kls(scc)
+
+ # Try card model #2
+ atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2"
+ if scc.get_atr() == toBytes(atr):
+ return kls(scc)
+
+ # Try card model #3
+ atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"
+ if scc.get_atr() == toBytes(atr):
+ return kls(scc)
+ except:
+ return None
+ return None
+
+ def verify_adm(self, key):
+ # authenticate as ADM using default key (written on the card..)
+ if not key:
+ raise ValueError(
+ "Please provide a PIN-ADM as there is no default one")
+ (res, sw) = self._scc.verify_chv(0x0A, key)
+ return sw
+
+ def program(self, p):
+ self.verify_adm(h2b(p['pin_adm']))
+
+ # Populate AIDs
+ self.read_aids()
+
+ # This type of card does not allow to reprogram the ICCID.
+ # Reprogramming the ICCID would mess up the card os software
+ # license management, so the ICCID must be kept at its factory
+ # setting!
+ if p.get('iccid'):
+ print(
+ "Warning: Programming of the ICCID is not implemented for this type of card.")
+
+ # select DF_GSM
+ self._scc.select_path(['7f20'])
+
+ # set Service Provider Name
+ if p.get('name') is not None:
+ self.update_spn(p['name'], True, True)
+
+ # write EF.IMSI
+ if p.get('imsi'):
+ self._scc.update_binary('6f07', enc_imsi(p['imsi']))
+
+ # EF.PLMNsel
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_plmnsel(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming PLMNsel failed with code %s" % sw)
+
+ # EF.PLMNwAcT
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_plmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming PLMNwAcT failed with code %s" % sw)
+
+ # EF.OPLMNwAcT
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_oplmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming OPLMNwAcT failed with code %s" % sw)
+
+ # EF.HPLMNwAcT
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_hplmn_act(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming HPLMNwAcT failed with code %s" % sw)
+
+ # EF.AD
+ if (p.get('mcc') and p.get('mnc')) or p.get('opmode'):
+ if p.get('mcc') and p.get('mnc'):
+ mnc = p['mnc']
+ else:
+ mnc = None
+ sw = self.update_ad(mnc=mnc, opmode=p.get('opmode'))
+ if sw != '9000':
+ print("Programming AD failed with code %s" % sw)
+
+ # EF.SMSP
+ if p.get('smsp'):
+ r = self._scc.select_path(['3f00', '7f10'])
+ data, sw = self._scc.update_record(
+ '6f42', 1, lpad(p['smsp'], 104), force_len=True)
+
+ # EF.MSISDN
+ # TODO: Alpha Identifier (currently 'ff'O * 20)
+ # TODO: Capability/Configuration1 Record Identifier
+ # TODO: Extension1 Record Identifier
+ if p.get('msisdn') is not None:
+ msisdn = enc_msisdn(p['msisdn'])
+ content = 'ff' * 20 + msisdn
+
+ r = self._scc.select_path(['3f00', '7f10'])
+ data, sw = self._scc.update_record(
+ '6F40', 1, content, force_len=True)
+
+ # EF.ACC
+ if p.get('acc'):
+ sw = self.update_acc(p['acc'])
+ if sw != '9000':
+ print("Programming ACC failed with code %s" % sw)
+
+ # update EF-SIM_AUTH_KEY (and EF-USIM_AUTH_KEY_2G, which is
+ # hard linked to EF-USIM_AUTH_KEY)
+ self._scc.select_path(['3f00'])
+ self._scc.select_path(['a515'])
+ if p.get('ki'):
+ self._scc.update_binary('6f20', p['ki'], 1)
+ if p.get('opc'):
+ self._scc.update_binary('6f20', p['opc'], 17)
+
+ # update EF-USIM_AUTH_KEY in ADF.ISIM
+ if self.adf_present("isim"):
+ self.select_adf_by_aid(adf="isim")
+
+ if p.get('ki'):
+ self._scc.update_binary('af20', p['ki'], 1)
+ if p.get('opc'):
+ self._scc.update_binary('af20', p['opc'], 17)
+
+ # update EF.P-CSCF in ADF.ISIM
+ if self.file_exists(EF_ISIM_ADF_map['PCSCF']):
+ if p.get('pcscf'):
+ sw = self.update_pcscf(p['pcscf'])
+ else:
+ sw = self.update_pcscf("")
+ if sw != '9000':
+ print("Programming P-CSCF failed with code %s" % sw)
+
+ # update EF.DOMAIN in ADF.ISIM
+ if self.file_exists(EF_ISIM_ADF_map['DOMAIN']):
+ if p.get('ims_hdomain'):
+ sw = self.update_domain(domain=p['ims_hdomain'])
+ else:
+ sw = self.update_domain()
+
+ if sw != '9000':
+ print(
+ "Programming Home Network Domain Name failed with code %s" % sw)
+
+ # update EF.IMPI in ADF.ISIM
+ # TODO: Validate IMPI input
+ if self.file_exists(EF_ISIM_ADF_map['IMPI']):
+ if p.get('impi'):
+ sw = self.update_impi(p['impi'])
+ else:
+ sw = self.update_impi()
+ if sw != '9000':
+ print("Programming IMPI failed with code %s" % sw)
+
+ # update EF.IMPU in ADF.ISIM
+ # TODO: Validate IMPU input
+ # Support multiple IMPU if there is enough space
+ if self.file_exists(EF_ISIM_ADF_map['IMPU']):
+ if p.get('impu'):
+ sw = self.update_impu(p['impu'])
+ else:
+ sw = self.update_impu()
+ if sw != '9000':
+ print("Programming IMPU failed with code %s" % sw)
+
+ if self.adf_present("usim"):
+ self.select_adf_by_aid(adf="usim")
+
+ # EF.AD in ADF.USIM
+ if (p.get('mcc') and p.get('mnc')) or p.get('opmode'):
+ if p.get('mcc') and p.get('mnc'):
+ mnc = p['mnc']
+ else:
+ mnc = None
+ sw = self.update_ad(mnc=mnc, opmode=p.get('opmode'),
+ path=EF_USIM_ADF_map['AD'])
+ if sw != '9000':
+ print("Programming AD failed with code %s" % sw)
+
+ # update EF-USIM_AUTH_KEY in ADF.USIM
+ if p.get('ki'):
+ self._scc.update_binary('af20', p['ki'], 1)
+ if p.get('opc'):
+ self._scc.update_binary('af20', p['opc'], 17)
+
+ # update EF.EHPLMN in ADF.USIM
+ if self.file_exists(EF_USIM_ADF_map['EHPLMN']):
+ if p.get('mcc') and p.get('mnc'):
+ sw = self.update_ehplmn(p['mcc'], p['mnc'])
+ if sw != '9000':
+ print("Programming EHPLMN failed with code %s" % sw)
+
+ # update EF.ePDGId in ADF.USIM
+ if self.file_exists(EF_USIM_ADF_map['ePDGId']):
+ if p.get('epdgid'):
+ sw = self.update_epdgid(p['epdgid'])
+ else:
+ sw = self.update_epdgid("")
+ if sw != '9000':
+ print("Programming ePDGId failed with code %s" % sw)
+
+ # update EF.ePDGSelection in ADF.USIM
+ if self.file_exists(EF_USIM_ADF_map['ePDGSelection']):
+ if p.get('epdgSelection'):
+ epdg_plmn = p['epdgSelection']
+ sw = self.update_ePDGSelection(
+ epdg_plmn[:3], epdg_plmn[3:])
+ else:
+ sw = self.update_ePDGSelection("", "")
+ if sw != '9000':
+ print("Programming ePDGSelection failed with code %s" % sw)
+
+ # After successfully programming EF.ePDGId and EF.ePDGSelection,
+ # Set service 106 and 107 as available in EF.UST
+ # Disable service 95, 99, 115 if ISIM application is present
+ if self.file_exists(EF_USIM_ADF_map['UST']):
+ if p.get('epdgSelection') and p.get('epdgid'):
+ sw = self.update_ust(106, 1)
+ if sw != '9000':
+ print("Programming UST failed with code %s" % sw)
+ sw = self.update_ust(107, 1)
+ if sw != '9000':
+ print("Programming UST failed with code %s" % sw)
+
+ sw = self.update_ust(95, 0)
+ if sw != '9000':
+ print("Programming UST failed with code %s" % sw)
+ sw = self.update_ust(99, 0)
+ if sw != '9000':
+ print("Programming UST failed with code %s" % sw)
+ sw = self.update_ust(115, 0)
+ if sw != '9000':
+ print("Programming UST failed with code %s" % sw)
+
+ return
+
+class SysmoISIMSJA5(SysmoISIMSJA2):
+ """
+ sysmocom sysmoISIM-SJA5
+ """
+
+ name = 'sysmoISIM-SJA5'
+
+ @classmethod
+ def autodetect(kls, scc):
+ try:
+ # Try card model #1 (9FJ)
+ atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC"
+ if scc.get_atr() == toBytes(atr):
+ return kls(scc)
+ # Try card model #2 (SLM17)
+ atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8"
+ if scc.get_atr() == toBytes(atr):
+ return kls(scc)
+ # Try card model #3 (9FV)
+ atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"
+ if scc.get_atr() == toBytes(atr):
+ return kls(scc)
+ except:
+ return None
+ return None
+
+
+class GialerSim(UsimCard):
+ """
+ Gialer sim cards (www.gialer.com).
+ """
+ name = 'gialersim'
+
+ def __init__(self, ssc):
+ super().__init__(ssc)
+ self._program_handlers = {
+ 'iccid': self.update_iccid,
+ 'imsi': self.update_imsi,
+ 'acc': self.update_acc,
+ 'smsp': self.update_smsp,
+ 'ki': self.update_ki,
+ 'opc': self.update_opc,
+ 'fplmn': self.update_fplmn,
+ }
+
+ @classmethod
+ def autodetect(cls, scc):
+ try:
+ # Look for ATR
+ if scc.get_atr() == toBytes('3B 9F 95 80 1F C7 80 31 A0 73 B6 A1 00 67 CF 32 15 CA 9C D7 09 20'):
+ return cls(scc)
+ except:
+ return None
+ return None
+
+ def program(self, p):
+ self.set_apdu_parameter('00', '0004')
+ # Authenticate
+ self._scc.verify_chv(0xc, h2b('3834373936313533'))
+ for handler in self._program_handlers:
+ if p.get(handler) is not None:
+ self._program_handlers[handler](p[handler])
+
+ mcc = p.get('mcc')
+ mnc = p.get('mnc')
+ has_plmn = mcc is not None and mnc is not None
+ # EF.HPLMN
+ if has_plmn:
+ self.update_hplmn_act(mcc, mnc)
+
+ # EF.AD
+ if has_plmn or (p.get('opmode') is not None):
+ self.update_ad(mnc=mnc, opmode=p.get('opmode'))
+
+ def update_smsp(self, smsp):
+ data, sw = self._scc.update_record(EF['SMSP'], 1, rpad(smsp, 80))
+ return sw
+
+ def update_ki(self, ki):
+ self._scc.select_path(['3f00', '0001'])
+ self._scc.update_binary('0001', ki)
+
+ def update_opc(self, opc):
+ self._scc.select_path(['3f00', '6002'])
+ # No idea why the '01' is required
+ self._scc.update_binary('6002', '01' + opc)
+
+
+# In order for autodetection ...
+_cards_classes = [FakeMagicSim, SuperSim, MagicSim, GrcardSim,
+ SysmoSIMgr1, SysmoSIMgr2, SysmoUSIMgr1, SysmoUSIMSJS1,
+ FairwavesSIM, OpenCellsSim, WavemobileSim, SysmoISIMSJA2,
+ SysmoISIMSJA5, GialerSim]
+
+
+def card_detect(ctype, scc):
+ # Detect type if needed
+ card = None
+ ctypes = dict([(kls.name, kls) for kls in _cards_classes])
+
+ if ctype == "auto":
+ for kls in _cards_classes:
+ card = kls.autodetect(scc)
+ if card:
+ print("Autodetected card type: %s" % card.name)
+ card.reset()
+ break
+
+ if card is None:
+ print("Autodetection failed")
+ return None
+
+ elif ctype in ctypes:
+ card = ctypes[ctype](scc)
+
+ else:
+ raise ValueError("Unknown card type: %s" % ctype)
+
+ return card
diff --git a/pySim/legacy/ts_31_102.py b/pySim/legacy/ts_31_102.py
new file mode 100644
index 0000000..c4a829e
--- /dev/null
+++ b/pySim/legacy/ts_31_102.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+
+"""
+Various constants from 3GPP TS 31.102 V17.9.0 usd by *legacy* code
+"""
+
+#
+# Copyright (C) 2020 Supreeth Herle <herlesupreeth@gmail.com>
+# Copyright (C) 2021-2023 Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+EF_USIM_ADF_map = {
+ 'LI': '6F05',
+ 'ARR': '6F06',
+ 'IMSI': '6F07',
+ 'Keys': '6F08',
+ 'KeysPS': '6F09',
+ 'DCK': '6F2C',
+ 'HPPLMN': '6F31',
+ 'CNL': '6F32',
+ 'ACMmax': '6F37',
+ 'UST': '6F38',
+ 'ACM': '6F39',
+ 'FDN': '6F3B',
+ 'SMS': '6F3C',
+ 'GID1': '6F3E',
+ 'GID2': '6F3F',
+ 'MSISDN': '6F40',
+ 'PUCT': '6F41',
+ 'SMSP': '6F42',
+ 'SMSS': '6F42',
+ 'CBMI': '6F45',
+ 'SPN': '6F46',
+ 'SMSR': '6F47',
+ 'CBMID': '6F48',
+ 'SDN': '6F49',
+ 'EXT2': '6F4B',
+ 'EXT3': '6F4C',
+ 'BDN': '6F4D',
+ 'EXT5': '6F4E',
+ 'CCP2': '6F4F',
+ 'CBMIR': '6F50',
+ 'EXT4': '6F55',
+ 'EST': '6F56',
+ 'ACL': '6F57',
+ 'CMI': '6F58',
+ 'START-HFN': '6F5B',
+ 'THRESHOLD': '6F5C',
+ 'PLMNwAcT': '6F60',
+ 'OPLMNwAcT': '6F61',
+ 'HPLMNwAcT': '6F62',
+ 'PSLOCI': '6F73',
+ 'ACC': '6F78',
+ 'FPLMN': '6F7B',
+ 'LOCI': '6F7E',
+ 'ICI': '6F80',
+ 'OCI': '6F81',
+ 'ICT': '6F82',
+ 'OCT': '6F83',
+ 'AD': '6FAD',
+ 'VGCS': '6FB1',
+ 'VGCSS': '6FB2',
+ 'VBS': '6FB3',
+ 'VBSS': '6FB4',
+ 'eMLPP': '6FB5',
+ 'AAeM': '6FB6',
+ 'ECC': '6FB7',
+ 'Hiddenkey': '6FC3',
+ 'NETPAR': '6FC4',
+ 'PNN': '6FC5',
+ 'OPL': '6FC6',
+ 'MBDN': '6FC7',
+ 'EXT6': '6FC8',
+ 'MBI': '6FC9',
+ 'MWIS': '6FCA',
+ 'CFIS': '6FCB',
+ 'EXT7': '6FCC',
+ 'SPDI': '6FCD',
+ 'MMSN': '6FCE',
+ 'EXT8': '6FCF',
+ 'MMSICP': '6FD0',
+ 'MMSUP': '6FD1',
+ 'MMSUCP': '6FD2',
+ 'NIA': '6FD3',
+ 'VGCSCA': '6FD4',
+ 'VBSCA': '6FD5',
+ 'GBAP': '6FD6',
+ 'MSK': '6FD7',
+ 'MUK': '6FD8',
+ 'EHPLMN': '6FD9',
+ 'GBANL': '6FDA',
+ 'EHPLMNPI': '6FDB',
+ 'LRPLMNSI': '6FDC',
+ 'NAFKCA': '6FDD',
+ 'SPNI': '6FDE',
+ 'PNNI': '6FDF',
+ 'NCP-IP': '6FE2',
+ 'EPSLOCI': '6FE3',
+ 'EPSNSC': '6FE4',
+ 'UFC': '6FE6',
+ 'UICCIARI': '6FE7',
+ 'NASCONFIG': '6FE8',
+ 'PWC': '6FEC',
+ 'FDNURI': '6FED',
+ 'BDNURI': '6FEE',
+ 'SDNURI': '6FEF',
+ 'IWL': '6FF0',
+ 'IPS': '6FF1',
+ 'IPD': '6FF2',
+ 'ePDGId': '6FF3',
+ 'ePDGSelection': '6FF4',
+ 'ePDGIdEm': '6FF5',
+ 'ePDGSelectionEm': '6FF6',
+}
+
+LOCI_STATUS_map = {
+ 0: 'updated',
+ 1: 'not updated',
+ 2: 'plmn not allowed',
+ 3: 'locatation area not allowed'
+}
diff --git a/pySim/legacy/ts_31_103.py b/pySim/legacy/ts_31_103.py
new file mode 100644
index 0000000..f500f08
--- /dev/null
+++ b/pySim/legacy/ts_31_103.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+
+"""
+Various constants from 3GPP TS 31.103 V16.1.0 used by *legacy* code only
+"""
+
+# Copyright (C) 2020 Supreeth Herle <herlesupreeth@gmail.com>
+# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+EF_ISIM_ADF_map = {
+ 'IST': '6F07',
+ 'IMPI': '6F02',
+ 'DOMAIN': '6F03',
+ 'IMPU': '6F04',
+ 'AD': '6FAD',
+ 'ARR': '6F06',
+ 'PCSCF': '6F09',
+ 'GBAP': '6FD5',
+ 'GBANL': '6FD7',
+ 'NAFKCA': '6FDD',
+ 'UICCIARI': '6FE7',
+ 'SMS': '6F3C',
+ 'SMSS': '6F43',
+ 'SMSR': '6F47',
+ 'SMSP': '6F42',
+ 'FromPreferred': '6FF7',
+ 'IMSConfigData': '6FF8',
+ 'XCAPConfigData': '6FFC',
+ 'WebRTCURI': '6FFA'
+}
diff --git a/pySim/legacy/ts_51_011.py b/pySim/legacy/ts_51_011.py
new file mode 100644
index 0000000..a384b1d
--- /dev/null
+++ b/pySim/legacy/ts_51_011.py
@@ -0,0 +1,257 @@
+# -*- coding: utf-8 -*-
+
+""" Various constants from 3GPP TS 51.011 used by *legacy* code only.
+
+This will likely be removed in future versions of pySim. Please instead
+use the pySim.filesystem class model with the various profile/application
+specific extension modules.
+"""
+
+# Copyright (C) 2017 Alexander.Chemeris <Alexander.Chemeris@gmail.com>
+# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+MF_num = '3F00'
+
+DF_num = {
+ 'TELECOM': '7F10',
+
+ 'GSM': '7F20',
+ 'IS-41': '7F22',
+ 'FP-CTS': '7F23',
+
+ 'GRAPHICS': '5F50',
+
+ 'IRIDIUM': '5F30',
+ 'GLOBST': '5F31',
+ 'ICO': '5F32',
+ 'ACeS': '5F33',
+
+ 'EIA/TIA-553': '5F40',
+ 'CTS': '5F60',
+ 'SOLSA': '5F70',
+
+ 'MExE': '5F3C',
+}
+
+EF_num = {
+ # MF
+ 'ICCID': '2FE2',
+ 'ELP': '2F05',
+ 'DIR': '2F00',
+
+ # DF_TELECOM
+ 'ADN': '6F3A',
+ 'FDN': '6F3B',
+ 'SMS': '6F3C',
+ 'CCP': '6F3D',
+ 'MSISDN': '6F40',
+ 'SMSP': '6F42',
+ 'SMSS': '6F43',
+ 'LND': '6F44',
+ 'SMSR': '6F47',
+ 'SDN': '6F49',
+ 'EXT1': '6F4A',
+ 'EXT2': '6F4B',
+ 'EXT3': '6F4C',
+ 'BDN': '6F4D',
+ 'EXT4': '6F4E',
+ 'CMI': '6F58',
+ 'ECCP': '6F4F',
+
+ # DF_GRAPHICS
+ 'IMG': '4F20',
+
+ # DF_SoLSA
+ 'SAI': '4F30',
+ 'SLL': '4F31',
+
+ # DF_MExE
+ 'MExE-ST': '4F40',
+ 'ORPK': '4F41',
+ 'ARPK': '4F42',
+ 'TPRPK': '4F43',
+
+ # DF_GSM
+ 'LP': '6F05',
+ 'IMSI': '6F07',
+ 'Kc': '6F20',
+ 'DCK': '6F2C',
+ 'PLMNsel': '6F30',
+ 'HPPLMN': '6F31',
+ 'CNL': '6F32',
+ 'ACMmax': '6F37',
+ 'SST': '6F38',
+ 'ACM': '6F39',
+ 'GID1': '6F3E',
+ 'GID2': '6F3F',
+ 'PUCT': '6F41',
+ 'CBMI': '6F45',
+ 'SPN': '6F46',
+ 'CBMID': '6F48',
+ 'BCCH': '6F74',
+ 'ACC': '6F78',
+ 'FPLMN': '6F7B',
+ 'LOCI': '6F7E',
+ 'AD': '6FAD',
+ 'PHASE': '6FAE',
+ 'VGCS': '6FB1',
+ 'VGCSS': '6FB2',
+ 'VBS': '6FB3',
+ 'VBSS': '6FB4',
+ 'eMLPP': '6FB5',
+ 'AAeM': '6FB6',
+ 'ECC': '6FB7',
+ 'CBMIR': '6F50',
+ 'NIA': '6F51',
+ 'KcGPRS': '6F52',
+ 'LOCIGPRS': '6F53',
+ 'SUME': '6F54',
+ 'PLMNwAcT': '6F60',
+ 'OPLMNwAcT': '6F61',
+ # Figure 8 names it HPLMNAcT, but in the text it's names it HPLMNwAcT
+ 'HPLMNAcT': '6F62',
+ 'HPLMNwAcT': '6F62',
+ 'CPBCCH': '6F63',
+ 'INVSCAN': '6F64',
+ 'PNN': '6FC5',
+ 'OPL': '6FC6',
+ 'MBDN': '6FC7',
+ 'EXT6': '6FC8',
+ 'MBI': '6FC9',
+ 'MWIS': '6FCA',
+ 'CFIS': '6FCB',
+ 'EXT7': '6FCC',
+ 'SPDI': '6FCD',
+ 'MMSN': '6FCE',
+ 'EXT8': '6FCF',
+ 'MMSICP': '6FD0',
+ 'MMSUP': '6FD1',
+ 'MMSUCP': '6FD2',
+}
+
+DF = {
+ 'TELECOM': [MF_num, DF_num['TELECOM']],
+
+ 'GSM': [MF_num, DF_num['GSM']],
+ 'IS-41': [MF_num, DF_num['IS-41']],
+ 'FP-CTS': [MF_num, DF_num['FP-CTS']],
+
+ 'GRAPHICS': [MF_num, DF_num['GRAPHICS']],
+
+ 'IRIDIUM': [MF_num, DF_num['IRIDIUM']],
+ 'GLOBST': [MF_num, DF_num['GLOBST']],
+ 'ICO': [MF_num, DF_num['ICO']],
+ 'ACeS': [MF_num, DF_num['ACeS']],
+
+ 'EIA/TIA-553': [MF_num, DF_num['EIA/TIA-553']],
+ 'CTS': [MF_num, DF_num['CTS']],
+ 'SoLSA': [MF_num, DF_num['SOLSA']],
+
+ 'MExE': [MF_num, DF_num['MExE']],
+}
+
+
+EF = {
+ 'ICCID': [MF_num, EF_num['ICCID']],
+ 'ELP': [MF_num, EF_num['ELP']],
+ 'DIR': [MF_num, EF_num['DIR']],
+
+ 'ADN': DF['TELECOM']+[EF_num['ADN']],
+ 'FDN': DF['TELECOM']+[EF_num['FDN']],
+ 'SMS': DF['TELECOM']+[EF_num['SMS']],
+ 'CCP': DF['TELECOM']+[EF_num['CCP']],
+ 'MSISDN': DF['TELECOM']+[EF_num['MSISDN']],
+ 'SMSP': DF['TELECOM']+[EF_num['SMSP']],
+ 'SMSS': DF['TELECOM']+[EF_num['SMSS']],
+ 'LND': DF['TELECOM']+[EF_num['LND']],
+ 'SMSR': DF['TELECOM']+[EF_num['SMSR']],
+ 'SDN': DF['TELECOM']+[EF_num['SDN']],
+ 'EXT1': DF['TELECOM']+[EF_num['EXT1']],
+ 'EXT2': DF['TELECOM']+[EF_num['EXT2']],
+ 'EXT3': DF['TELECOM']+[EF_num['EXT3']],
+ 'BDN': DF['TELECOM']+[EF_num['BDN']],
+ 'EXT4': DF['TELECOM']+[EF_num['EXT4']],
+ 'CMI': DF['TELECOM']+[EF_num['CMI']],
+ 'ECCP': DF['TELECOM']+[EF_num['ECCP']],
+
+ 'IMG': DF['GRAPHICS']+[EF_num['IMG']],
+
+ 'SAI': DF['SoLSA']+[EF_num['SAI']],
+ 'SLL': DF['SoLSA']+[EF_num['SLL']],
+
+ 'MExE-ST': DF['MExE']+[EF_num['MExE-ST']],
+ 'ORPK': DF['MExE']+[EF_num['ORPK']],
+ 'ARPK': DF['MExE']+[EF_num['ARPK']],
+ 'TPRPK': DF['MExE']+[EF_num['TPRPK']],
+
+ 'LP': DF['GSM']+[EF_num['LP']],
+ 'IMSI': DF['GSM']+[EF_num['IMSI']],
+ 'Kc': DF['GSM']+[EF_num['Kc']],
+ 'DCK': DF['GSM']+[EF_num['DCK']],
+ 'PLMNsel': DF['GSM']+[EF_num['PLMNsel']],
+ 'HPPLMN': DF['GSM']+[EF_num['HPPLMN']],
+ 'CNL': DF['GSM']+[EF_num['CNL']],
+ 'ACMmax': DF['GSM']+[EF_num['ACMmax']],
+ 'SST': DF['GSM']+[EF_num['SST']],
+ 'ACM': DF['GSM']+[EF_num['ACM']],
+ 'GID1': DF['GSM']+[EF_num['GID1']],
+ 'GID2': DF['GSM']+[EF_num['GID2']],
+ 'PUCT': DF['GSM']+[EF_num['PUCT']],
+ 'CBMI': DF['GSM']+[EF_num['CBMI']],
+ 'SPN': DF['GSM']+[EF_num['SPN']],
+ 'CBMID': DF['GSM']+[EF_num['CBMID']],
+ 'BCCH': DF['GSM']+[EF_num['BCCH']],
+ 'ACC': DF['GSM']+[EF_num['ACC']],
+ 'FPLMN': DF['GSM']+[EF_num['FPLMN']],
+ 'LOCI': DF['GSM']+[EF_num['LOCI']],
+ 'AD': DF['GSM']+[EF_num['AD']],
+ 'PHASE': DF['GSM']+[EF_num['PHASE']],
+ 'VGCS': DF['GSM']+[EF_num['VGCS']],
+ 'VGCSS': DF['GSM']+[EF_num['VGCSS']],
+ 'VBS': DF['GSM']+[EF_num['VBS']],
+ 'VBSS': DF['GSM']+[EF_num['VBSS']],
+ 'eMLPP': DF['GSM']+[EF_num['eMLPP']],
+ 'AAeM': DF['GSM']+[EF_num['AAeM']],
+ 'ECC': DF['GSM']+[EF_num['ECC']],
+ 'CBMIR': DF['GSM']+[EF_num['CBMIR']],
+ 'NIA': DF['GSM']+[EF_num['NIA']],
+ 'KcGPRS': DF['GSM']+[EF_num['KcGPRS']],
+ 'LOCIGPRS': DF['GSM']+[EF_num['LOCIGPRS']],
+ 'SUME': DF['GSM']+[EF_num['SUME']],
+ 'PLMNwAcT': DF['GSM']+[EF_num['PLMNwAcT']],
+ 'OPLMNwAcT': DF['GSM']+[EF_num['OPLMNwAcT']],
+ # Figure 8 names it HPLMNAcT, but in the text it's names it HPLMNwAcT
+ 'HPLMNAcT': DF['GSM']+[EF_num['HPLMNAcT']],
+ 'HPLMNwAcT': DF['GSM']+[EF_num['HPLMNAcT']],
+ 'CPBCCH': DF['GSM']+[EF_num['CPBCCH']],
+ 'INVSCAN': DF['GSM']+[EF_num['INVSCAN']],
+ 'PNN': DF['GSM']+[EF_num['PNN']],
+ 'OPL': DF['GSM']+[EF_num['OPL']],
+ 'MBDN': DF['GSM']+[EF_num['MBDN']],
+ 'EXT6': DF['GSM']+[EF_num['EXT6']],
+ 'MBI': DF['GSM']+[EF_num['MBI']],
+ 'MWIS': DF['GSM']+[EF_num['MWIS']],
+ 'CFIS': DF['GSM']+[EF_num['CFIS']],
+ 'EXT7': DF['GSM']+[EF_num['EXT7']],
+ 'SPDI': DF['GSM']+[EF_num['SPDI']],
+ 'MMSN': DF['GSM']+[EF_num['MMSN']],
+ 'EXT8': DF['GSM']+[EF_num['EXT8']],
+ 'MMSICP': DF['GSM']+[EF_num['MMSICP']],
+ 'MMSUP': DF['GSM']+[EF_num['MMSUP']],
+ 'MMSUCP': DF['GSM']+[EF_num['MMSUCP']],
+}
+
diff --git a/pySim/legacy/utils.py b/pySim/legacy/utils.py
new file mode 100644
index 0000000..cf2416e
--- /dev/null
+++ b/pySim/legacy/utils.py
@@ -0,0 +1,332 @@
+# -*- coding: utf-8 -*-
+
+""" pySim: various utilities only used by legacy tools (pySim-{prog,read})
+"""
+
+# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
+# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+from pySim.utils import Hexstr, rpad, enc_plmn, h2i, i2s, s2h
+from pySim.utils import dec_xplmn_w_act, dec_xplmn, dec_mcc_from_plmn, dec_mnc_from_plmn
+
+def hexstr_to_Nbytearr(s, nbytes):
+ return [s[i:i+(nbytes*2)] for i in range(0, len(s), (nbytes*2))]
+
+def format_xplmn_w_act(hexstr):
+ s = ""
+ for rec_data in hexstr_to_Nbytearr(hexstr, 5):
+ rec_info = dec_xplmn_w_act(rec_data)
+ if rec_info['mcc'] == "" and rec_info['mnc'] == "":
+ rec_str = "unused"
+ else:
+ rec_str = "MCC: %s MNC: %s AcT: %s" % (
+ rec_info['mcc'], rec_info['mnc'], ", ".join(rec_info['act']))
+ s += "\t%s # %s\n" % (rec_data, rec_str)
+ return s
+
+
+def format_xplmn(hexstr: Hexstr) -> str:
+ s = ""
+ for rec_data in hexstr_to_Nbytearr(hexstr, 3):
+ rec_info = dec_xplmn(rec_data)
+ if not rec_info['mcc'] and not rec_info['mnc']:
+ rec_str = "unused"
+ else:
+ rec_str = "MCC: %s MNC: %s" % (rec_info['mcc'], rec_info['mnc'])
+ s += "\t%s # %s\n" % (rec_data, rec_str)
+ return s
+
+
+def format_ePDGSelection(hexstr):
+ ePDGSelection_info_tag_chars = 2
+ ePDGSelection_info_tag_str = hexstr[:2]
+ s = ""
+ # Minimum length
+ len_chars = 2
+ # TODO: Need to determine length properly - definite length support only
+ # Inconsistency in spec: 3GPP TS 31.102 version 15.2.0 Release 15, 4.2.104
+ # As per spec, length is 5n, n - number of PLMNs
+ # But, each PLMN entry is made of PLMN (3 Bytes) + ePDG Priority (2 Bytes) + ePDG FQDN format (1 Byte)
+ # Totalling to 6 Bytes, maybe length should be 6n
+ len_str = hexstr[ePDGSelection_info_tag_chars:ePDGSelection_info_tag_chars+len_chars]
+
+ # Not programmed scenario
+ if int(len_str, 16) == 255 or int(ePDGSelection_info_tag_str, 16) == 255:
+ len_chars = 0
+ ePDGSelection_info_tag_chars = 0
+ if len_str[0] == '8':
+ # The bits 7 to 1 denotes the number of length octets if length > 127
+ if int(len_str[1]) > 0:
+ # Update number of length octets
+ len_chars = len_chars * int(len_str[1])
+ len_str = hexstr[ePDGSelection_info_tag_chars:len_chars]
+
+ content_str = hexstr[ePDGSelection_info_tag_chars+len_chars:]
+ # Right pad to prevent index out of range - multiple of 6 bytes
+ content_str = rpad(content_str, len(content_str) +
+ (12 - (len(content_str) % 12)))
+ for rec_data in hexstr_to_Nbytearr(content_str, 6):
+ rec_info = dec_ePDGSelection(rec_data)
+ if rec_info['mcc'] == 0xFFF and rec_info['mnc'] == 0xFFF:
+ rec_str = "unused"
+ else:
+ rec_str = "MCC: %03d MNC: %03d ePDG Priority: %s ePDG FQDN format: %s" % \
+ (rec_info['mcc'], rec_info['mnc'],
+ rec_info['epdg_priority'], rec_info['epdg_fqdn_format'])
+ s += "\t%s # %s\n" % (rec_data, rec_str)
+ return s
+
+def enc_st(st, service, state=1):
+ """
+ Encodes the EF S/U/IST/EST and returns the updated Service Table
+
+ Parameters:
+ st - Current value of SIM/USIM/ISIM Service Table
+ service - Service Number to encode as activated/de-activated
+ state - 1 mean activate, 0 means de-activate
+
+ Returns:
+ s - Modified value of SIM/USIM/ISIM Service Table
+
+ Default values:
+ - state: 1 - Sets the particular Service bit to 1
+ """
+ st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
+
+ s = ""
+ # Check whether the requested service is present in each byte
+ for i in range(0, len(st_bytes)):
+ # Byte i contains info about Services num (8i+1) to num (8i+8)
+ if service in range((8*i) + 1, (8*i) + 9):
+ byte = int(st_bytes[i], 16)
+ # Services in each byte are in order MSB to LSB
+ # MSB - Service (8i+8)
+ # LSB - Service (8i+1)
+ mod_byte = 0x00
+ # Copy bit by bit contents of byte to mod_byte with modified bit
+ # for requested service
+ for j in range(1, 9):
+ mod_byte = mod_byte >> 1
+ if service == (8*i) + j:
+ mod_byte = state == 1 and mod_byte | 0x80 or mod_byte & 0x7f
+ else:
+ mod_byte = byte & 0x01 == 0x01 and mod_byte | 0x80 or mod_byte & 0x7f
+ byte = byte >> 1
+
+ s += ('%02x' % (mod_byte))
+ else:
+ s += st_bytes[i]
+
+ return s
+
+
+def dec_st(st, table="sim") -> str:
+ """
+ Parses the EF S/U/IST and prints the list of available services in EF S/U/IST
+ """
+
+ if table == "isim":
+ from pySim.ts_31_103 import EF_IST_map
+ lookup_map = EF_IST_map
+ elif table == "usim":
+ from pySim.ts_31_102 import EF_UST_map
+ lookup_map = EF_UST_map
+ else:
+ from pySim.ts_51_011 import EF_SST_map
+ lookup_map = EF_SST_map
+
+ st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
+
+ avail_st = ""
+ # Get each byte and check for available services
+ for i in range(0, len(st_bytes)):
+ # Byte i contains info about Services num (8i+1) to num (8i+8)
+ byte = int(st_bytes[i], 16)
+ # Services in each byte are in order MSB to LSB
+ # MSB - Service (8i+8)
+ # LSB - Service (8i+1)
+ for j in range(1, 9):
+ if byte & 0x01 == 0x01 and ((8*i) + j in lookup_map):
+ # Byte X contains info about Services num (8X-7) to num (8X)
+ # bit = 1: service available
+ # bit = 0: service not available
+ avail_st += '\tService %d - %s\n' % (
+ (8*i) + j, lookup_map[(8*i) + j])
+ byte = byte >> 1
+ return avail_st
+
+
+def enc_ePDGSelection(hexstr, mcc, mnc, epdg_priority='0001', epdg_fqdn_format='00'):
+ """
+ Encode ePDGSelection so it can be stored at EF.ePDGSelection or EF.ePDGSelectionEm.
+ See 3GPP TS 31.102 version 15.2.0 Release 15, section 4.2.104 and 4.2.106.
+
+ Default values:
+ - epdg_priority: '0001' - 1st Priority
+ - epdg_fqdn_format: '00' - Operator Identifier FQDN
+ """
+
+ plmn1 = enc_plmn(mcc, mnc) + epdg_priority + epdg_fqdn_format
+ # TODO: Handle encoding of Length field for length more than 127 Bytes
+ content = '80' + ('%02x' % (len(plmn1)//2)) + plmn1
+ content = rpad(content, len(hexstr))
+ return content
+
+
+def dec_ePDGSelection(sixhexbytes):
+ """
+ Decode ePDGSelection to get EF.ePDGSelection or EF.ePDGSelectionEm.
+ See 3GPP TS 31.102 version 15.2.0 Release 15, section 4.2.104 and 4.2.106.
+ """
+
+ res = {'mcc': 0, 'mnc': 0, 'epdg_priority': 0, 'epdg_fqdn_format': ''}
+ plmn_chars = 6
+ epdg_priority_chars = 4
+ epdg_fqdn_format_chars = 2
+ # first three bytes (six ascii hex chars)
+ plmn_str = sixhexbytes[:plmn_chars]
+ # two bytes after first three bytes
+ epdg_priority_str = sixhexbytes[plmn_chars:plmn_chars +
+ epdg_priority_chars]
+ # one byte after first five bytes
+ epdg_fqdn_format_str = sixhexbytes[plmn_chars +
+ epdg_priority_chars:plmn_chars + epdg_priority_chars + epdg_fqdn_format_chars]
+ res['mcc'] = dec_mcc_from_plmn(plmn_str)
+ res['mnc'] = dec_mnc_from_plmn(plmn_str)
+ res['epdg_priority'] = epdg_priority_str
+ res['epdg_fqdn_format'] = epdg_fqdn_format_str == '00' and 'Operator Identifier FQDN' or 'Location based FQDN'
+ return res
+
+
+def first_TLV_parser(bytelist):
+ '''
+ first_TLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) -> (170, 2, [171, 205])
+
+ parses first TLV format record in a list of bytelist
+ returns a 3-Tuple: Tag, Length, Value
+ Value is a list of bytes
+ parsing of length is ETSI'style 101.220
+ '''
+ Tag = bytelist[0]
+ if bytelist[1] == 0xFF:
+ Len = bytelist[2]*256 + bytelist[3]
+ Val = bytelist[4:4+Len]
+ else:
+ Len = bytelist[1]
+ Val = bytelist[2:2+Len]
+ return (Tag, Len, Val)
+
+
+def TLV_parser(bytelist):
+ '''
+ TLV_parser([0xAA, ..., 0xFF]) -> [(T, L, [V]), (T, L, [V]), ...]
+
+ loops on the input list of bytes with the "first_TLV_parser()" function
+ returns a list of 3-Tuples
+ '''
+ ret = []
+ while len(bytelist) > 0:
+ T, L, V = first_TLV_parser(bytelist)
+ if T == 0xFF:
+ # padding bytes
+ break
+ ret.append((T, L, V))
+ # need to manage length of L
+ if L > 0xFE:
+ bytelist = bytelist[L+4:]
+ else:
+ bytelist = bytelist[L+2:]
+ return ret
+
+
+def dec_addr_tlv(hexstr):
+ """
+ Decode hex string to get EF.P-CSCF Address or EF.ePDGId or EF.ePDGIdEm.
+ See 3GPP TS 31.102 version 13.4.0 Release 13, section 4.2.8, 4.2.102 and 4.2.104.
+ """
+
+ # Convert from hex str to int bytes list
+ addr_tlv_bytes = h2i(hexstr)
+
+ # Get list of tuples containing parsed TLVs
+ tlvs = TLV_parser(addr_tlv_bytes)
+
+ for tlv in tlvs:
+ # tlv = (T, L, [V])
+ # T = Tag
+ # L = Length
+ # [V] = List of value
+
+ # Invalid Tag value scenario
+ if tlv[0] != 0x80:
+ continue
+
+ # Empty field - Zero length
+ if tlv[1] == 0:
+ continue
+
+ # Uninitialized field
+ if all([v == 0xff for v in tlv[2]]):
+ continue
+
+ # First byte in the value has the address type
+ addr_type = tlv[2][0]
+ # TODO: Support parsing of IPv6
+ # Address Type: 0x00 (FQDN), 0x01 (IPv4), 0x02 (IPv6), other (Reserved)
+ if addr_type == 0x00: # FQDN
+ # Skip address tye byte i.e. first byte in value list
+ content = tlv[2][1:]
+ return (i2s(content), '00')
+
+ elif addr_type == 0x01: # IPv4
+ # Skip address tye byte i.e. first byte in value list
+ # Skip the unused byte in Octect 4 after address type byte as per 3GPP TS 31.102
+ ipv4 = tlv[2][2:]
+ content = '.'.join(str(x) for x in ipv4)
+ return (content, '01')
+ else:
+ raise ValueError("Invalid address type")
+
+ return (None, None)
+
+
+def enc_addr_tlv(addr, addr_type='00'):
+ """
+ Encode address TLV object used in EF.P-CSCF Address, EF.ePDGId and EF.ePDGIdEm.
+ See 3GPP TS 31.102 version 13.4.0 Release 13, section 4.2.8, 4.2.102 and 4.2.104.
+
+ Default values:
+ - addr_type: 00 - FQDN format of Address
+ """
+
+ s = ""
+
+ # TODO: Encoding of IPv6 address
+ if addr_type == '00': # FQDN
+ hex_str = s2h(addr)
+ s += '80' + ('%02x' % ((len(hex_str)//2)+1)) + '00' + hex_str
+ elif addr_type == '01': # IPv4
+ ipv4_list = addr.split('.')
+ ipv4_str = ""
+ for i in ipv4_list:
+ ipv4_str += ('%02x' % (int(i)))
+
+ # Unused bytes shall be set to 'ff'. i.e 4th Octet after Address Type is not used
+ # IPv4 Address is in octet 5 to octet 8 of the TLV data object
+ s += '80' + ('%02x' % ((len(ipv4_str)//2)+2)) + '01' + 'ff' + ipv4_str
+
+ return s
diff --git a/pySim/ota.py b/pySim/ota.py
index 5955a08..afc94cc 100644
--- a/pySim/ota.py
+++ b/pySim/ota.py
@@ -15,14 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from pySim.construct import *
-from pySim.utils import b2h
-from pySim.sms import UserDataHeader
-from construct import *
import zlib
import abc
import struct
from typing import Optional
+from construct import Enum, Int8ub, Int16ub, Struct, Bytes, GreedyBytes, BitsInteger, BitStruct
+from construct import Flag, Padding, Switch, this
+
+from pySim.construct import *
+from pySim.utils import b2h
+from pySim.sms import UserDataHeader
# ETS TS 102 225 gives the general command structure and the dialects for CAT_TP, TCP/IP and HTTPS
# 3GPP TS 31.115 gives the dialects for SMS-PP, SMS-CB, USSD and HTTP
@@ -112,12 +114,12 @@ class OtaKeyset:
@property
def auth(self):
"""Return an instance of the matching OtaAlgoAuth."""
- return OtaAlgoAuth.fromKeyset(self)
+ return OtaAlgoAuth.from_keyset(self)
@property
def crypt(self):
"""Return an instance of the matching OtaAlgoCrypt."""
- return OtaAlgoCrypt.fromKeyset(self)
+ return OtaAlgoCrypt.from_keyset(self)
class OtaCheckError(Exception):
pass
@@ -128,26 +130,24 @@ class OtaDialect(abc.ABC):
def _compute_sig_len(self, spi:SPI):
if spi['rc_cc_ds'] == 'no_rc_cc_ds':
return 0
- elif spi['rc_cc_ds'] == 'rc': # CRC-32
+ if spi['rc_cc_ds'] == 'rc': # CRC-32
return 4
- elif spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
+ if spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
# TODO: this is not entirely correct, as in AES case it could be 4 or 8
return 8
- else:
- raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
+ raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
@abc.abstractmethod
- def encode_cmd(self, otak: OtaKeyset, tar: bytes, apdu: bytes) -> bytes:
+ def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
pass
@abc.abstractmethod
- def decode_resp(self, otak: OtaKeyset, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
+ def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
"""Decode a response into a response packet and, if indicted (by a
response status of `"por_ok"`) a decoded response.
The response packet's common characteristics are not fully determined,
and (so far) completely proprietary per dialect."""
- pass
from Cryptodome.Cipher import DES, DES3, AES
@@ -190,7 +190,7 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
def encrypt(self, data:bytes) -> bytes:
"""Encrypt given input bytes using the key material given in constructor."""
padded_data = self.pad_to_blocksize(data)
- return self._encrypt(data)
+ return self._encrypt(padded_data)
def decrypt(self, data:bytes) -> bytes:
"""Decrypt given input bytes using the key material given in constructor."""
@@ -199,15 +199,13 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
@abc.abstractmethod
def _encrypt(self, data:bytes) -> bytes:
"""Actual implementation, to be implemented by derived class."""
- pass
@abc.abstractmethod
def _decrypt(self, data:bytes) -> bytes:
"""Actual implementation, to be implemented by derived class."""
- pass
@classmethod
- def fromKeyset(cls, otak: OtaKeyset) -> 'OtaAlgoCrypt':
+ def from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoCrypt':
"""Resolve the class for the encryption algorithm of otak and instantiate it."""
for subc in cls.__subclasses__():
if subc.enum_name == otak.algo_crypt:
@@ -239,7 +237,7 @@ class OtaAlgoAuth(OtaAlgo, abc.ABC):
pass
@classmethod
- def fromKeyset(cls, otak: OtaKeyset) -> 'OtaAlgoAuth':
+ def from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoAuth':
"""Resolve the class for the authentication algorithm of otak and instantiate it."""
for subc in cls.__subclasses__():
if subc.enum_name == otak.algo_auth:
@@ -293,7 +291,7 @@ class OtaAlgoAuthDES3(OtaAlgoAuth):
class OtaAlgoCryptAES(OtaAlgoCrypt):
name = 'AES'
enum_name = 'aes_cbc'
- blocksize = 16 # TODO: is this needed?
+ blocksize = 16
def _encrypt(self, data:bytes) -> bytes:
cipher = AES.new(self.otak.kic, AES.MODE_CBC, self.iv)
return cipher.encrypt(data)
@@ -346,20 +344,20 @@ class OtaDialectSms(OtaDialect):
# CHL + SPI (+ KIC + KID)
c = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
part_head = c.build({'chl': chl, 'spi':spi, 'kic':kic, 'kid':kid, 'tar':tar})
- #print("part_head: %s" % b2h(part_head))
+ print("part_head: %s" % b2h(part_head))
# CNTR + PCNTR (CNTR not used)
part_cnt = otak.cntr.to_bytes(5, 'big') + pad_cnt.to_bytes(1, 'big')
- #print("part_cnt: %s" % b2h(part_cnt))
+ print("part_cnt: %s" % b2h(part_cnt))
envelope_data = part_head + part_cnt + apdu
- #print("envelope_data: %s" % b2h(envelope_data))
+ print("envelope_data: %s" % b2h(envelope_data))
# 2-byte CPL. CPL is part of RC/CC/CPI to end of secured data, including any padding for ciphering
# CPL from and including CPI to end of secured data, including any padding for ciphering
cpl = len(envelope_data) + len_sig
envelope_data = cpl.to_bytes(2, 'big') + envelope_data
- #print("envelope_data with cpl: %s" % b2h(envelope_data))
+ print("envelope_data with cpl: %s" % b2h(envelope_data))
if spi['rc_cc_ds'] == 'cc':
cc = otak.auth.sign(envelope_data)
@@ -373,7 +371,7 @@ class OtaDialectSms(OtaDialect):
else:
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
- #print("envelope_data with sig: %s" % b2h(envelope_data))
+ print("envelope_data with sig: %s" % b2h(envelope_data))
# encrypt as needed
if spi['ciphering']: # ciphering is requested
@@ -385,7 +383,7 @@ class OtaDialectSms(OtaDialect):
else:
envelope_data = part_head + envelope_data
- #print("envelope_data: %s" % b2h(envelope_data))
+ print("envelope_data: %s" % b2h(envelope_data))
return envelope_data
@@ -399,7 +397,7 @@ class OtaDialectSms(OtaDialect):
# POR with CC+CIPH: 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
if data[0] != 0x02:
raise ValueError('Unexpected UDL=0x%02x' % data[0])
- udhd, remainder = UserDataHeader.fromBytes(data)
+ udhd, remainder = UserDataHeader.from_bytes(data)
if not udhd.has_ie(0x71):
raise ValueError('RPI 0x71 not found in UDH')
rph_rhl_tar = remainder[:6] # RPH+RHL+TAR; not ciphered
diff --git a/pySim/profile.py b/pySim/profile.py
index e464e1f..74946ba 100644
--- a/pySim/profile.py
+++ b/pySim/profile.py
@@ -21,13 +21,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-from pySim.commands import SimCardCommands
-from pySim.filesystem import CardApplication, interpret_sw
-from pySim.utils import all_subclasses
import abc
import operator
from typing import List
+from pySim.commands import SimCardCommands
+from pySim.filesystem import CardApplication, interpret_sw
+from pySim.utils import all_subclasses
def _mf_select_test(scc: SimCardCommands,
cla_byte: str, sel_ctrl: str,
@@ -88,6 +88,7 @@ class CardProfile:
shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
cla : class byte that should be used with cards of this profile
sel_ctrl : selection control bytes class byte that should be used with cards of this profile
+ addons: List of optional CardAddons that a card of this profile might have
"""
self.name = name
self.desc = kw.get("desc", None)
@@ -97,6 +98,8 @@ class CardProfile:
self.shell_cmdsets = kw.get("shell_cmdsets", [])
self.cla = kw.get("cla", "00")
self.sel_ctrl = kw.get("sel_ctrl", "0004")
+ # list of optional addons that a card of this profile might have
+ self.addons = kw.get("addons", [])
def __str__(self):
return self.name
@@ -161,3 +164,32 @@ class CardProfile:
return p()
return None
+
+ def add_addon(self, addon: 'CardProfileAddon'):
+ assert addon not in self.addons
+ # we don't install any additional files, as that is happening in the RuntimeState.
+ self.addons.append(addon)
+
+class CardProfileAddon(abc.ABC):
+ """A Card Profile Add-on is something that is not a card application or a full stand-alone
+ card profile, but an add-on to an existing profile. Think of GSM-R specific files existing
+ on what is otherwise a SIM or USIM+SIM card."""
+
+ def __init__(self, name: str, **kw):
+ """
+ Args:
+ desc (str) : Description
+ files_in_mf : List of CardEF instances present in MF
+ shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
+ """
+ self.name = name
+ self.desc = kw.get("desc", None)
+ self.files_in_mf = kw.get("files_in_mf", [])
+ self.shell_cmdsets = kw.get("shell_cmdsets", [])
+
+ def __str__(self):
+ return self.name
+
+ @abc.abstractmethod
+ def probe(self, card: 'CardBase') -> bool:
+ """Probe a given card to determine whether or not this add-on is present/supported."""
diff --git a/pySim/runtime.py b/pySim/runtime.py
new file mode 100644
index 0000000..3ef9b2d
--- /dev/null
+++ b/pySim/runtime.py
@@ -0,0 +1,550 @@
+# coding=utf-8
+"""Representation of the runtime state of an application like pySim-shell.
+"""
+
+# (C) 2021 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from typing import Optional, Tuple
+
+from pySim.utils import h2b, i2h, is_hex, bertlv_parse_one, Hexstr
+from pySim.exceptions import *
+from pySim.filesystem import *
+
+def lchan_nr_from_cla(cla: int) -> int:
+ """Resolve the logical channel number from the CLA byte."""
+ # TS 102 221 10.1.1 Coding of Class Byte
+ if cla >> 4 in [0x0, 0xA, 0x8]:
+ # Table 10.3
+ return cla & 0x03
+ if cla & 0xD0 in [0x40, 0xC0]:
+ # Table 10.4a
+ return 4 + (cla & 0x0F)
+ raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
+
+class RuntimeState:
+ """Represent the runtime state of a session with a card."""
+
+ def __init__(self, card: 'CardBase', profile: 'CardProfile'):
+ """
+ Args:
+ card : pysim.cards.Card instance
+ profile : CardProfile instance
+ """
+ self.mf = CardMF(profile=profile)
+ self.card = card
+ self.profile = profile
+ self.lchan = {}
+ # the basic logical channel always exists
+ self.lchan[0] = RuntimeLchan(0, self)
+ # this is a dict of card identities which different parts of the code might populate,
+ # typically with something like ICCID, EID, ATR, ...
+ self.identity = {}
+
+ # make sure the class and selection control bytes, which are specified
+ # by the card profile are used
+ self.card.set_apdu_parameter(
+ cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
+
+ for addon_cls in self.profile.addons:
+ addon = addon_cls()
+ if addon.probe(self.card):
+ print("Detected %s Add-on \"%s\"" % (self.profile, addon))
+ for f in addon.files_in_mf:
+ self.mf.add_file(f)
+
+ # go back to MF before the next steps (addon probing might have changed DF)
+ self.lchan[0].select('MF')
+
+ # add application ADFs + MF-files from profile
+ apps = self._match_applications()
+ for a in apps:
+ if a.adf:
+ self.mf.add_application_df(a.adf)
+ for f in self.profile.files_in_mf:
+ self.mf.add_file(f)
+ self.conserve_write = True
+
+ # make sure that when the runtime state is created, the card is also
+ # in a defined state.
+ self.reset()
+
+ def _match_applications(self):
+ """match the applications from the profile with applications on the card"""
+ apps_profile = self.profile.applications
+
+ # When the profile does not feature any applications, then we are done already
+ if not apps_profile:
+ return []
+
+ # Read AIDs from card and match them against the applications defined by the
+ # card profile
+ aids_card = self.card.read_aids()
+ apps_taken = []
+ if aids_card:
+ aids_taken = []
+ print("AIDs on card:")
+ for a in aids_card:
+ for f in apps_profile:
+ if f.aid in a:
+ print(" %s: %s (EF.DIR)" % (f.name, a))
+ aids_taken.append(a)
+ apps_taken.append(f)
+ aids_unknown = set(aids_card) - set(aids_taken)
+ for a in aids_unknown:
+ print(" unknown: %s (EF.DIR)" % a)
+ else:
+ print("warning: EF.DIR seems to be empty!")
+
+ # Some card applications may not be registered in EF.DIR, we will actively
+ # probe for those applications
+ for f in sorted(set(apps_profile) - set(apps_taken), key=str):
+ try:
+ # we can not use the lchan provided methods select, or select_file
+ # since those method work on an already finished file model. At
+ # this point we are still in the initialization process, so it is
+ # no problem when we access the card object directly without caring
+ # about updating other states. For normal selects at runtime, the
+ # caller must use the lchan provided methods select or select_file!
+ _data, sw = self.card.select_adf_by_aid(f.aid)
+ self.selected_adf = f
+ if sw == "9000":
+ print(" %s: %s" % (f.name, f.aid))
+ apps_taken.append(f)
+ except (SwMatchError, ProtocolError):
+ pass
+ return apps_taken
+
+ def reset(self, cmd_app=None) -> Hexstr:
+ """Perform physical card reset and obtain ATR.
+ Args:
+ cmd_app : Command Application State (for unregistering old file commands)
+ """
+ # delete all lchan != 0 (basic lchan)
+ for lchan_nr in list(self.lchan.keys()):
+ if lchan_nr == 0:
+ continue
+ del self.lchan[lchan_nr]
+ atr = i2h(self.card.reset())
+ # select MF to reset internal state and to verify card really works
+ self.lchan[0].select('MF', cmd_app)
+ self.lchan[0].selected_adf = None
+ # store ATR as part of our card identies dict
+ self.identity['ATR'] = atr
+ return atr
+
+ def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
+ """Add a logical channel to the runtime state. You shouldn't call this
+ directly but always go through RuntimeLchan.add_lchan()."""
+ if lchan_nr in self.lchan.keys():
+ raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
+ self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
+ return self.lchan[lchan_nr]
+
+ def del_lchan(self, lchan_nr: int):
+ if lchan_nr in self.lchan.keys():
+ del self.lchan[lchan_nr]
+ return True
+ else:
+ return False
+
+ def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
+ lchan_nr = lchan_nr_from_cla(cla)
+ if lchan_nr in self.lchan.keys():
+ return self.lchan[lchan_nr]
+ else:
+ return None
+
+
+class RuntimeLchan:
+ """Represent the runtime state of a logical channel with a card."""
+
+ def __init__(self, lchan_nr: int, rs: RuntimeState):
+ self.lchan_nr = lchan_nr
+ self.rs = rs
+ self.scc = self.rs.card._scc.fork_lchan(lchan_nr)
+
+ # File reference data
+ self.selected_file = self.rs.mf
+ self.selected_adf = None
+ self.selected_file_fcp = None
+ self.selected_file_fcp_hex = None
+
+ def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
+ """Add a new logical channel from the current logical channel. Just affects
+ internal state, doesn't actually open a channel with the UICC."""
+ new_lchan = self.rs.add_lchan(lchan_nr)
+ # See TS 102 221 Table 8.3
+ if self.lchan_nr != 0:
+ new_lchan.selected_file = self.get_cwd()
+ new_lchan.selected_adf = self.selected_adf
+ return new_lchan
+
+ def selected_file_descriptor_byte(self) -> dict:
+ return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
+
+ def selected_file_shareable(self) -> bool:
+ return self.selected_file_descriptor_byte()['shareable']
+
+ def selected_file_structure(self) -> str:
+ return self.selected_file_descriptor_byte()['structure']
+
+ def selected_file_type(self) -> str:
+ return self.selected_file_descriptor_byte()['file_type']
+
+ def selected_file_num_of_rec(self) -> Optional[int]:
+ return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
+
+ def get_cwd(self) -> CardDF:
+ """Obtain the current working directory.
+
+ Returns:
+ CardDF instance
+ """
+ if isinstance(self.selected_file, CardDF):
+ return self.selected_file
+ else:
+ return self.selected_file.parent
+
+ def get_application_df(self) -> Optional[CardADF]:
+ """Obtain the currently selected application DF (if any).
+
+ Returns:
+ CardADF() instance or None"""
+ # iterate upwards from selected file; check if any is an ADF
+ node = self.selected_file
+ while node.parent != node:
+ if isinstance(node, CardADF):
+ return node
+ node = node.parent
+ return None
+
+ def interpret_sw(self, sw: str):
+ """Interpret a given status word relative to the currently selected application
+ or the underlying card profile.
+
+ Args:
+ sw : Status word as string of 4 hex digits
+
+ Returns:
+ Tuple of two strings
+ """
+ res = None
+ adf = self.get_application_df()
+ if adf:
+ app = adf.application
+ # The application either comes with its own interpret_sw
+ # method or we will use the interpret_sw method from the
+ # card profile.
+ if app and hasattr(app, "interpret_sw"):
+ res = app.interpret_sw(sw)
+ return res or self.rs.profile.interpret_sw(sw)
+
+ def probe_file(self, fid: str, cmd_app=None):
+ """Blindly try to select a file and automatically add a matching file
+ object if the file actually exists."""
+ if not is_hex(fid, 4, 4):
+ raise ValueError(
+ "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
+
+ self._select_pre(cmd_app)
+
+ try:
+ # We access the card through the select_file method of the scc object.
+ # If we succeed, we know that the file exists on the card and we may
+ # proceed with creating a new CardEF object in the local file model at
+ # run time. In case the file does not exist on the card, we just abort.
+ # The state on the card (selected file/application) wont't be changed,
+ # so we do not have to update any state in that case.
+ (data, _sw) = self.scc.select_file(fid)
+ except SwMatchError as swm:
+ self._select_post(cmd_app)
+ k = self.interpret_sw(swm.sw_actual)
+ if not k:
+ raise swm
+ raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1])) from swm
+
+ select_resp = self.selected_file.decode_select_response(data)
+ if select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df':
+ f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
+ desc="dedicated file, manually added at runtime")
+ else:
+ if select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent':
+ f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
+ desc="elementary file, manually added at runtime")
+ else:
+ f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
+ desc="elementary file, manually added at runtime")
+
+ self.selected_file.add_files([f])
+
+ self._select_post(cmd_app, f, data)
+
+ def _select_pre(self, cmd_app):
+ # unregister commands of old file
+ if cmd_app and self.selected_file.shell_commands:
+ for c in self.selected_file.shell_commands:
+ cmd_app.unregister_command_set(c)
+
+ def _select_post(self, cmd_app, file:Optional[CardFile] = None, select_resp_data = None):
+ # we store some reference data (see above) about the currently selected file.
+ # This data must be updated after every select.
+ if file:
+ self.selected_file = file
+ if isinstance(file, CardADF):
+ self.selected_adf = file
+ if select_resp_data:
+ self.selected_file_fcp_hex = select_resp_data
+ self.selected_file_fcp = self.selected_file.decode_select_response(select_resp_data)
+ else:
+ self.selected_file_fcp_hex = None
+ self.selected_file_fcp = None
+
+ # register commands of new file
+ if cmd_app and self.selected_file.shell_commands:
+ for c in self.selected_file.shell_commands:
+ cmd_app.register_command_set(c)
+
+ def select_file(self, file: CardFile, cmd_app=None):
+ """Select a file (EF, DF, ADF, MF, ...).
+
+ Args:
+ file : CardFile [or derived class] instance
+ cmd_app : Command Application State (for unregistering old file commands)
+ """
+ # we need to find a path from our self.selected_file to the destination
+ inter_path = self.selected_file.build_select_path_to(file)
+ if not inter_path:
+ raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
+ self._select_pre(cmd_app)
+
+ # be sure the variables that we pass to _select_post contain valid values.
+ selected_file = self.selected_file
+ data = self.selected_file_fcp_hex
+
+ for f in inter_path:
+ try:
+ # We now directly accessing the card to perform the selection. This
+ # will change the state of the card, so we must take care to update
+ # the local state (lchan) as well. This is done in the method
+ # _select_post. It should be noted that the caller must always use
+ # the methods select_file or select. The caller must not access the
+ # card directly since this would lead into an incoherence of the
+ # card state and the state of the lchan.
+ if isinstance(f, CardADF):
+ (data, _sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
+ else:
+ (data, _sw) = self.scc.select_file(f.fid)
+ selected_file = f
+ except SwMatchError as swm:
+ self._select_post(cmd_app, selected_file, data)
+ raise swm
+
+ self._select_post(cmd_app, f, data)
+
+ def select(self, name: str, cmd_app=None):
+ """Select a file (EF, DF, ADF, MF, ...).
+
+ Args:
+ name : Name of file to select
+ cmd_app : Command Application State (for unregistering old file commands)
+ """
+ # if any intermediate step fails, we must be able to go back where we were
+ prev_sel_file = self.selected_file
+
+ # handling of entire paths with multiple directories/elements
+ if '/' in name:
+ pathlist = name.split('/')
+ # treat /DF.GSM/foo like MF/DF.GSM/foo
+ if pathlist[0] == '':
+ pathlist[0] = 'MF'
+ try:
+ for p in pathlist:
+ self.select(p, cmd_app)
+ return self.selected_file_fcp
+ except Exception as e:
+ self.select_file(prev_sel_file, cmd_app)
+ raise e
+
+ # we are now in the directory where the target file is located
+ # so we can now refer to the get_selectables() method to get the
+ # file object and select it using select_file()
+ sels = self.selected_file.get_selectables()
+ if is_hex(name):
+ name = name.lower()
+
+ try:
+ if name in sels:
+ self.select_file(sels[name], cmd_app)
+ else:
+ self.probe_file(name, cmd_app)
+ except Exception as e:
+ self.select_file(prev_sel_file, cmd_app)
+ raise e
+
+ return self.selected_file_fcp
+
+ def status(self):
+ """Request STATUS (current selected file FCP) from card."""
+ (data, _sw) = self.scc.status()
+ return self.selected_file.decode_select_response(data)
+
+ def get_file_for_selectable(self, name: str):
+ sels = self.selected_file.get_selectables()
+ return sels[name]
+
+ def activate_file(self, name: str):
+ """Request ACTIVATE FILE of specified file."""
+ sels = self.selected_file.get_selectables()
+ f = sels[name]
+ data, sw = self.scc.activate_file(f.fid)
+ return data, sw
+
+ def read_binary(self, length: int = None, offset: int = 0):
+ """Read [part of] a transparent EF binary data.
+
+ Args:
+ length : Amount of data to read (None: as much as possible)
+ offset : Offset into the file from which to read 'length' bytes
+ Returns:
+ binary data read from the file
+ """
+ if not isinstance(self.selected_file, TransparentEF):
+ raise TypeError("Only works with TransparentEF")
+ return self.scc.read_binary(self.selected_file.fid, length, offset)
+
+ def read_binary_dec(self) -> Tuple[dict, str]:
+ """Read [part of] a transparent EF binary data and decode it.
+
+ Args:
+ length : Amount of data to read (None: as much as possible)
+ offset : Offset into the file from which to read 'length' bytes
+ Returns:
+ abstract decode data read from the file
+ """
+ (data, sw) = self.read_binary()
+ dec_data = self.selected_file.decode_hex(data)
+ return (dec_data, sw)
+
+ def update_binary(self, data_hex: str, offset: int = 0):
+ """Update transparent EF binary data.
+
+ Args:
+ data_hex : hex string of data to be written
+ offset : Offset into the file from which to write 'data_hex'
+ """
+ if not isinstance(self.selected_file, TransparentEF):
+ raise TypeError("Only works with TransparentEF")
+ return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
+
+ def update_binary_dec(self, data: dict):
+ """Update transparent EF from abstract data. Encodes the data to binary and
+ then updates the EF with it.
+
+ Args:
+ data : abstract data which is to be encoded and written
+ """
+ data_hex = self.selected_file.encode_hex(data)
+ return self.update_binary(data_hex)
+
+ def read_record(self, rec_nr: int = 0):
+ """Read a record as binary data.
+
+ Args:
+ rec_nr : Record number to read
+ Returns:
+ hex string of binary data contained in record
+ """
+ if not isinstance(self.selected_file, LinFixedEF):
+ raise TypeError("Only works with Linear Fixed EF")
+ # returns a string of hex nibbles
+ return self.scc.read_record(self.selected_file.fid, rec_nr)
+
+ def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
+ """Read a record and decode it to abstract data.
+
+ Args:
+ rec_nr : Record number to read
+ Returns:
+ abstract data contained in record
+ """
+ (data, sw) = self.read_record(rec_nr)
+ return (self.selected_file.decode_record_hex(data, rec_nr), sw)
+
+ def update_record(self, rec_nr: int, data_hex: str):
+ """Update a record with given binary data
+
+ Args:
+ rec_nr : Record number to read
+ data_hex : Hex string binary data to be written
+ """
+ if not isinstance(self.selected_file, LinFixedEF):
+ raise TypeError("Only works with Linear Fixed EF")
+ return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
+ conserve=self.rs.conserve_write,
+ leftpad=self.selected_file.leftpad)
+
+ def update_record_dec(self, rec_nr: int, data: dict):
+ """Update a record with given abstract data. Will encode abstract to binary data
+ and then write it to the given record on the card.
+
+ Args:
+ rec_nr : Record number to read
+ data_hex : Abstract data to be written
+ """
+ data_hex = self.selected_file.encode_record_hex(data, rec_nr)
+ return self.update_record(rec_nr, data_hex)
+
+ def retrieve_data(self, tag: int = 0):
+ """Read a DO/TLV as binary data.
+
+ Args:
+ tag : Tag of TLV/DO to read
+ Returns:
+ hex string of full BER-TLV DO including Tag and Length
+ """
+ if not isinstance(self.selected_file, BerTlvEF):
+ raise TypeError("Only works with BER-TLV EF")
+ # returns a string of hex nibbles
+ return self.scc.retrieve_data(self.selected_file.fid, tag)
+
+ def retrieve_tags(self):
+ """Retrieve tags available on BER-TLV EF.
+
+ Returns:
+ list of integer tags contained in EF
+ """
+ if not isinstance(self.selected_file, BerTlvEF):
+ raise TypeError("Only works with BER-TLV EF")
+ data, _sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
+ _tag, _length, value, _remainder = bertlv_parse_one(h2b(data))
+ return list(value)
+
+ def set_data(self, tag: int, data_hex: str):
+ """Update a TLV/DO with given binary data
+
+ Args:
+ tag : Tag of TLV/DO to be written
+ data_hex : Hex string binary data to be written (value portion)
+ """
+ if not isinstance(self.selected_file, BerTlvEF):
+ raise TypeError("Only works with BER-TLV EF")
+ return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
+
+ def unregister_cmds(self, cmd_app=None):
+ """Unregister all file specific commands."""
+ if cmd_app and self.selected_file.shell_commands:
+ for c in self.selected_file.shell_commands:
+ cmd_app.unregister_command_set(c)
diff --git a/pySim/secure_channel.py b/pySim/secure_channel.py
new file mode 100644
index 0000000..974780e
--- /dev/null
+++ b/pySim/secure_channel.py
@@ -0,0 +1,37 @@
+# Generic code related to Secure Channel processing
+#
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import abc
+from pySim.utils import b2h, h2b, ResTuple, Hexstr
+
+class SecureChannel(abc.ABC):
+ @abc.abstractmethod
+ def wrap_cmd_apdu(self, apdu: bytes) -> bytes:
+ """Wrap Command APDU according to specific Secure Channel Protocol."""
+ pass
+
+ @abc.abstractmethod
+ def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
+ """UnWrap Response-APDU according to specific Secure Channel Protocol."""
+ pass
+
+ def send_apdu_wrapper(self, send_fn: callable, pdu: Hexstr, *args, **kwargs) -> ResTuple:
+ """Wrapper function to wrap command APDU and unwrap repsonse APDU around send_apdu callable."""
+ pdu_wrapped = b2h(self.wrap_cmd_apdu(h2b(pdu)))
+ res, sw = send_fn(pdu_wrapped, *args, **kwargs)
+ res_unwrapped = b2h(self.unwrap_rsp_apdu(h2b(sw), h2b(res)))
+ return res_unwrapped, sw
diff --git a/pySim/sms.py b/pySim/sms.py
index 88ee27a..5ba5ec8 100644
--- a/pySim/sms.py
+++ b/pySim/sms.py
@@ -20,7 +20,7 @@
import typing
import abc
from bidict import bidict
-from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger, Flag
+from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger
from construct import Struct, Enum, Tell, BitStruct, this, Padding
from construct import Prefixed, GreedyRange, GreedyBytes
@@ -51,13 +51,13 @@ class UserDataHeader:
return False
@classmethod
- def fromBytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
+ def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
if isinstance(inb, str):
inb = h2b(inb)
res = cls._construct.parse(inb)
return cls(res['ies']), res['data']
- def toBytes(self) -> bytes:
+ def to_bytes(self) -> bytes:
return self._construct.build({'ies':self.ies, 'data':b''})
@@ -117,7 +117,7 @@ class AddressField:
return 'AddressField(TON=%s, NPI=%s, %s)' % (self.ton, self.npi, self.digits)
@classmethod
- def fromBytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
+ def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
"""Construct an AddressField instance from the binary T-PDU address format."""
if isinstance(inb, str):
inb = h2b(inb)
@@ -129,16 +129,16 @@ class AddressField:
return cls(res['digits'][:res['addr_len']], ton, npi), inb[res['tell']:]
@classmethod
- def fromSmpp(cls, addr, ton, npi) -> 'AddressField':
+ def from_smpp(cls, addr, ton, npi) -> 'AddressField':
"""Construct an AddressField from {source,dest}_addr_{,ton,npi} attributes of smpp.pdu."""
# return the resulting instance
return cls(addr.decode('ascii'), AddressField.smpp_map_ton[ton.name], AddressField.smpp_map_npi[npi.name])
- def toSmpp(self):
+ def to_smpp(self):
"""Return smpp.pdo.*.source,dest}_addr_{,ton,npi} attributes for given AddressField."""
return (self.digits, self.smpp_map_ton.inverse[self.ton], self.smpp_map_npi.inverse[self.npi])
- def toBytes(self) -> bytes:
+ def to_bytes(self) -> bytes:
"""Encode the AddressField into the binary representation as used in T-PDU."""
num_digits = len(self.digits)
if num_digits % 2:
@@ -185,13 +185,12 @@ class SMS_DELIVER(SMS_TPDU):
return '%s(MTI=%s, MMS=%s, LP=%s, RP=%s, UDHI=%s, SRI=%s, OA=%s, PID=%2x, DCS=%x, SCTS=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_mms, self.tp_lp, self.tp_rp, self.tp_udhi, self.tp_sri, self.tp_oa, self.tp_pid, self.tp_dcs, self.tp_scts, self.tp_udl, self.tp_ud)
@classmethod
- def fromBytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
+ def from_bytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
"""Construct a SMS_DELIVER instance from the binary encoded format as used in T-PDU."""
if isinstance(inb, str):
inb = h2b(inb)
- flags = inb[0]
d = SMS_DELIVER.flags_construct.parse(inb)
- oa, remainder = AddressField.fromBytes(inb[1:])
+ oa, remainder = AddressField.from_bytes(inb[1:])
d['tp_oa'] = oa
offset = 0
d['tp_pid'] = remainder[offset]
@@ -206,7 +205,7 @@ class SMS_DELIVER(SMS_TPDU):
d['tp_ud'] = remainder[offset:]
return cls(**d)
- def toBytes(self) -> bytes:
+ def to_bytes(self) -> bytes:
"""Encode a SMS_DELIVER instance to the binary encoded format as used in T-PDU."""
outb = bytearray()
d = {
@@ -215,7 +214,7 @@ class SMS_DELIVER(SMS_TPDU):
}
flags = SMS_DELIVER.flags_construct.build(d)
outb.extend(flags)
- outb.extend(self.tp_oa.toBytes())
+ outb.extend(self.tp_oa.to_bytes())
outb.append(self.tp_pid)
outb.append(self.tp_dcs)
outb.extend(self.tp_scts)
@@ -225,18 +224,18 @@ class SMS_DELIVER(SMS_TPDU):
return outb
@classmethod
- def fromSmpp(cls, smpp_pdu) -> 'SMS_DELIVER':
+ def from_smpp(cls, smpp_pdu) -> 'SMS_DELIVER':
"""Construct a SMS_DELIVER instance from the deliver format used by smpp.pdu."""
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
- return cls.fromSmppSubmit(smpp_pdu)
+ return cls.from_smpp_submit(smpp_pdu)
else:
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
@classmethod
- def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_DELIVER':
+ def from_smpp_submit(cls, smpp_pdu) -> 'SMS_DELIVER':
"""Construct a SMS_DELIVER instance from the submit format used by smpp.pdu."""
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
- tp_oa = AddressField.fromSmpp(smpp_pdu.params['source_addr'],
+ tp_oa = AddressField.from_smpp(smpp_pdu.params['source_addr'],
smpp_pdu.params['source_addr_ton'],
smpp_pdu.params['source_addr_npi'])
tp_ud = smpp_pdu.params['short_message']
@@ -276,7 +275,7 @@ class SMS_SUBMIT(SMS_TPDU):
return '%s(MTI=%s, RD=%s, VPF=%u, RP=%s, UDHI=%s, SRR=%s, DA=%s, PID=%2x, DCS=%x, VP=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_rd, self.tp_vpf, self.tp_rp, self.tp_udhi, self.tp_srr, self.tp_da, self.tp_pid, self.tp_dcs, self.tp_vp, self.tp_udl, self.tp_ud)
@classmethod
- def fromBytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
+ def from_bytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
"""Construct a SMS_SUBMIT instance from the binary encoded format as used in T-PDU."""
offset = 0
if isinstance(inb, str):
@@ -285,7 +284,7 @@ class SMS_SUBMIT(SMS_TPDU):
offset += 1
d['tp_mr']= inb[offset]
offset += 1
- da, remainder = AddressField.fromBytes(inb[2:])
+ da, remainder = AddressField.from_bytes(inb[2:])
d['tp_da'] = da
offset = 0
@@ -303,12 +302,10 @@ class SMS_SUBMIT(SMS_TPDU):
# TODO: further decode
d['tp_vp'] = remainder[offset:offset+7]
offset += 7
- pass
elif d['tp_vpf'] == 'absolute':
# TODO: further decode
d['tp_vp'] = remainder[offset:offset+7]
offset += 7
- pass
else:
raise ValueError('Invalid VPF: %s' % d['tp_vpf'])
d['tp_udl'] = remainder[offset]
@@ -316,7 +313,7 @@ class SMS_SUBMIT(SMS_TPDU):
d['tp_ud'] = remainder[offset:]
return cls(**d)
- def toBytes(self) -> bytes:
+ def to_bytes(self) -> bytes:
"""Encode a SMS_SUBMIT instance to the binary encoded format as used in T-PDU."""
outb = bytearray()
d = {
@@ -326,7 +323,7 @@ class SMS_SUBMIT(SMS_TPDU):
flags = SMS_SUBMIT.flags_construct.build(d)
outb.extend(flags)
outb.append(self.tp_mr)
- outb.extend(self.tp_da.toBytes())
+ outb.extend(self.tp_da.to_bytes())
outb.append(self.tp_pid)
outb.append(self.tp_dcs)
if self.tp_vpf != 'none':
@@ -336,20 +333,20 @@ class SMS_SUBMIT(SMS_TPDU):
return outb
@classmethod
- def fromSmpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
+ def from_smpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
"""Construct a SMS_SUBMIT instance from the format used by smpp.pdu."""
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
- return cls.fromSmppSubmit(smpp_pdu)
+ return cls.from_smpp_submit(smpp_pdu)
else:
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
@classmethod
- def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_SUBMIT':
+ def from_smpp_submit(cls, smpp_pdu) -> 'SMS_SUBMIT':
"""Construct a SMS_SUBMIT instance from the submit format used by smpp.pdu."""
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
- tp_da = AddressField.fromSmpp(smpp_pdu.params['destination_addr'],
- smpp_pdu.params['dest_addr_ton'],
- smpp_pdu.params['dest_addr_npi'])
+ tp_da = AddressField.from_smpp(smpp_pdu.params['destination_addr'],
+ smpp_pdu.params['dest_addr_ton'],
+ smpp_pdu.params['dest_addr_npi'])
tp_ud = smpp_pdu.params['short_message']
#vp_smpp = smpp_pdu.params['validity_period']
#if not vp_smpp:
@@ -370,7 +367,7 @@ class SMS_SUBMIT(SMS_TPDU):
}
return cls(**d)
- def toSmpp(self) -> pdu_types.PDU:
+ def to_smpp(self) -> pdu_types.PDU:
"""Translate a SMS_SUBMIT instance to a smpp.pdu.operations.SubmitSM instance."""
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT)
reg_del = pdu_types.RegisteredDelivery(pdu_types.RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED)
@@ -382,7 +379,7 @@ class SMS_SUBMIT(SMS_TPDU):
if self.tp_dcs != 0xF6:
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
dc = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT, pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
- (daddr, ton, npi) = self.tp_da.toSmpp()
+ (daddr, ton, npi) = self.tp_da.to_smpp()
return operations.SubmitSM(service_type='',
source_addr_ton=pdu_types.AddrTon.ALPHANUMERIC,
source_addr_npi=pdu_types.AddrNpi.UNKNOWN,
diff --git a/pySim/sysmocom_sja2.py b/pySim/sysmocom_sja2.py
index 080b87a..f2ae431 100644
--- a/pySim/sysmocom_sja2.py
+++ b/pySim/sysmocom_sja2.py
@@ -17,13 +17,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-from pytlv.TLV import *
-from struct import pack, unpack
+from struct import unpack
+from construct import FlagsEnum, Byte, Struct, Int8ub, Bytes, Mapping, Enum, Padding, BitsInteger
+from construct import Bit, this, Int32ub, Int16ub, Nibble, BytesInteger, GreedyRange, Const
+from construct import Optional as COptional
+
from pySim.utils import *
from pySim.filesystem import *
-from pySim.ts_102_221 import CardProfileUICC
+from pySim.runtime import RuntimeState
from pySim.construct import *
-from construct import *
import pySim
key_type2str = {
@@ -45,42 +47,48 @@ mac_length = {
class EF_PIN(TransparentEF):
- def __init__(self, fid, name):
+ _test_de_encode = [
+ ( 'f1030331323334ffffffff0a0a3132333435363738',
+ { 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
+ 'not_initialized': False, 'disabled': True },
+ 'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '31323334',
+ 'puk': { 'attempts_remaining': 10, 'maximum_attempts': 10, 'puk': '3132333435363738' }
+ } ),
+ ( 'f003039999999999999999',
+ { 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
+ 'not_initialized': False, 'disabled': False },
+ 'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '9999999999999999',
+ 'puk': None } ),
+ ]
+ def __init__(self, fid='6f01', name='EF.CHV1'):
super().__init__(fid, name=name, desc='%s PIN file' % name)
-
- def _decode_bin(self, raw_bin_data):
- u = unpack('!BBB8s', raw_bin_data[:11])
- res = {'enabled': (True, False)[u[0] & 0x01],
- 'initialized': (True, False)[u[0] & 0x02],
- 'disable_able': (False, True)[u[0] & 0x10],
- 'unblock_able': (False, True)[u[0] & 0x20],
- 'change_able': (False, True)[u[0] & 0x40],
- 'valid': (False, True)[u[0] & 0x80],
- 'attempts_remaining': u[1],
- 'maximum_attempts': u[2],
- 'pin': u[3].hex(),
- }
- if len(raw_bin_data) == 21:
- u2 = unpack('!BB8s', raw_bin_data[11:10])
- res['attempts_remaining_puk'] = u2[0]
- res['maximum_attempts_puk'] = u2[1]
- res['puk'] = u2[2].hex()
- return res
+ StateByte = FlagsEnum(Byte, disabled=1, not_initialized=2, disable_able=0x10, unblock_able=0x20,
+ change_able=0x40, valid=0x80)
+ PukStruct = Struct('attempts_remaining'/Int8ub,
+ 'maximum_attempts'/Int8ub,
+ 'puk'/HexAdapter(Rpad(Bytes(8))))
+ self._construct = Struct('state'/StateByte,
+ 'attempts_remaining'/Int8ub,
+ 'maximum_attempts'/Int8ub,
+ 'pin'/HexAdapter(Rpad(Bytes(8))),
+ 'puk'/COptional(PukStruct))
class EF_MILENAGE_CFG(TransparentEF):
+ _test_de_encode = [
+ ( '40002040600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000400000000000000000000000000000008',
+ {"r1": 64, "r2": 0, "r3": 32, "r4": 64, "r5": 96, "c1": "00000000000000000000000000000000", "c2":
+ "00000000000000000000000000000001", "c3": "00000000000000000000000000000002", "c4":
+ "00000000000000000000000000000004", "c5": "00000000000000000000000000000008"} ),
+ ]
def __init__(self, fid='6f21', name='EF.MILENAGE_CFG', desc='Milenage connfiguration'):
super().__init__(fid, name=name, desc=desc)
-
- def _decode_bin(self, raw_bin_data):
- u = unpack('!BBBBB16s16s16s16s16s', raw_bin_data)
- return {'r1': u[0], 'r2': u[1], 'r3': u[2], 'r4': u[3], 'r5': u[4],
- 'c1': u[5].hex(),
- 'c2': u[6].hex(),
- 'c3': u[7].hex(),
- 'c4': u[8].hex(),
- 'c5': u[9].hex(),
- }
+ self._construct = Struct('r1'/Int8ub, 'r2'/Int8ub, 'r3'/Int8ub, 'r4'/Int8ub, 'r5'/Int8ub,
+ 'c1'/HexAdapter(Bytes(16)),
+ 'c2'/HexAdapter(Bytes(16)),
+ 'c3'/HexAdapter(Bytes(16)),
+ 'c4'/HexAdapter(Bytes(16)),
+ 'c5'/HexAdapter(Bytes(16)))
class EF_0348_KEY(LinFixedEF):
@@ -98,12 +106,14 @@ class EF_0348_KEY(LinFixedEF):
class EF_0348_COUNT(LinFixedEF):
+ _test_de_encode = [
+ ( 'fe010000000000', {"sec_domain": 254, "key_set_version": 1, "counter": "0000000000"} ),
+ ]
def __init__(self, fid='6f23', name='EF.0348_COUNT', desc='TS 03.48 OTA Counters'):
super().__init__(fid, name=name, desc=desc, rec_len=(7, 7))
-
- def _decode_record_bin(self, raw_bin_data, **kwargs):
- u = unpack('!BB5s', raw_bin_data)
- return {'sec_domain': u[0], 'key_set_version': u[1], 'counter': u[2]}
+ self._construct = Struct('sec_domain'/Int8ub,
+ 'key_set_version'/Int8ub,
+ 'counter'/HexAdapter(Bytes(5)))
class EF_SIM_AUTH_COUNTER(TransparentEF):
@@ -113,13 +123,15 @@ class EF_SIM_AUTH_COUNTER(TransparentEF):
class EF_GP_COUNT(LinFixedEF):
+ _test_de_encode = [
+ ( '0070000000', {"sec_domain": 0, "key_set_version": 112, "counter": 0, "rfu": 0} ),
+ ]
def __init__(self, fid='6f26', name='EF.GP_COUNT', desc='GP SCP02 Counters'):
super().__init__(fid, name=name, desc=desc, rec_len=(5, 5))
-
- def _decode_record_bin(self, raw_bin_data, **kwargs):
- u = unpack('!BBHB', raw_bin_data)
- return {'sec_domain': u[0], 'key_set_version': u[1], 'counter': u[2], 'rfu': u[3]}
-
+ self._construct = Struct('sec_domain'/Int8ub,
+ 'key_set_version'/Int8ub,
+ 'counter'/Int16ub,
+ 'rfu'/Int8ub)
class EF_GP_DIV_DATA(LinFixedEF):
def __init__(self, fid='6f27', name='EF.GP_DIV_DATA', desc='GP SCP02 key diversification data'):
@@ -131,11 +143,16 @@ class EF_GP_DIV_DATA(LinFixedEF):
class EF_SIM_AUTH_KEY(TransparentEF):
+ _test_de_encode = [
+ ( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ {"cfg": {"sres_deriv_func": 1, "use_opc_instead_of_op": True, "algorithm": "milenage"}, "key":
+ "000102030405060708090a0b0c0d0e0f", "op_opc": "101112131415161718191a1b1c1d1e1f"} ),
+ ]
def __init__(self, fid='6f20', name='EF.SIM_AUTH_KEY'):
super().__init__(fid, name=name, desc='USIM authentication key')
CfgByte = BitStruct(Padding(2),
- 'use_sres_deriv_func_2'/Bit,
- 'use_opc_instead_of_op'/Bit,
+ 'sres_deriv_func'/Mapping(Bit, {1:0, 2:1}),
+ 'use_opc_instead_of_op'/Flag,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
self._construct = Struct('cfg'/CfgByte,
'key'/HexAdapter(Bytes(16)),
@@ -192,13 +209,21 @@ class DF_SYSTEM(CardDF):
class EF_USIM_SQN(TransparentEF):
+ _test_de_encode = [
+ ( 'd503000200000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
+ {"flag1": {"skip_next_sqn_check": True, "delta_max_check": True, "age_limit_check": False, "sqn_check": True,
+ "ind_len": 5}, "flag2": {"rfu": 0, "dont_clear_amf_for_macs": False, "aus_concealed": True,
+ "autn_concealed": True}, "delta_max": 8589934592, "age_limit":
+ 8589934592, "freshness": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0]} ),
+ ]
def __init__(self, fid='af30', name='EF.USIM_SQN'):
super().__init__(fid, name=name, desc='SQN parameters for AKA')
- Flag1 = BitStruct('skip_next_sqn_check'/Bit, 'delta_max_check'/Bit,
- 'age_limit_check'/Bit, 'sqn_check'/Bit,
+ Flag1 = BitStruct('skip_next_sqn_check'/Flag, 'delta_max_check'/Flag,
+ 'age_limit_check'/Flag, 'sqn_check'/Flag,
'ind_len'/BitsInteger(4))
- Flag2 = BitStruct('rfu'/BitsRFU(5), 'dont_clear_amf_for_macs'/Bit,
- 'aus_concealed'/Bit, 'autn_concealed'/Bit)
+ Flag2 = BitStruct('rfu'/BitsRFU(5), 'dont_clear_amf_for_macs'/Flag,
+ 'aus_concealed'/Flag, 'autn_concealed'/Flag)
self._construct = Struct('flag1'/Flag1, 'flag2'/Flag2,
'delta_max' /
BytesInteger(6), 'age_limit'/BytesInteger(6),
@@ -206,11 +231,23 @@ class EF_USIM_SQN(TransparentEF):
class EF_USIM_AUTH_KEY(TransparentEF):
+ _test_de_encode = [
+ ( '141898d827f70120d33b3e7462ee5fd6fe6ca53d7a0a804561646816d7b0c702fb',
+ { "cfg": { "only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True, "algorithm": "milenage" },
+ "key": "1898d827f70120d33b3e7462ee5fd6fe", "op_opc": "6ca53d7a0a804561646816d7b0c702fb" } ),
+ ( '160a04101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f000102030405060708090a0b0c0d0e0f',
+ { "cfg" : { "algorithm" : "tuak", "key_length" : 128, "sres_deriv_func_in_2g" : 1, "use_opc_instead_of_op" : True },
+ "tuak_cfg" : { "ck_and_ik_size" : 128, "mac_size" : 128, "res_size" : 128 },
+ "num_of_keccak_iterations" : 4,
+ "k" : "000102030405060708090a0b0c0d0e0f",
+ "op_opc" : "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
+ } ),
+ ]
def __init__(self, fid='af20', name='EF.USIM_AUTH_KEY'):
super().__init__(fid, name=name, desc='USIM authentication key')
Algorithm = Enum(Nibble, milenage=4, sha1_aka=5, tuak=6, xor=15)
- CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Bit,
- 'sres_deriv_func_2_in_3g'/Mapping(Bit, {1:0, 2:1}),
+ CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Flag,
+ 'sres_deriv_func_in_2g'/Mapping(Bit, {1:0, 2:1}),
'use_opc_instead_of_op'/Mapping(Bit, {False:0, True:1}),
'algorithm'/Algorithm)
self._construct = Struct('cfg'/CfgByte,
@@ -221,7 +258,7 @@ class EF_USIM_AUTH_KEY(TransparentEF):
# the TUAK and non-TUAK situation
CfgByteTuak = BitStruct(Padding(1),
'key_length'/Mapping(Bit, {128:0, 256:1}),
- 'sres_deriv_func_in_3g'/Mapping(Bit, {1:0, 2:1}),
+ 'sres_deriv_func_in_2g'/Mapping(Bit, {1:0, 2:1}),
'use_opc_instead_of_op'/Mapping(Bit, {False:0, True:1}),
'algorithm'/Algorithm)
TuakCfgByte = BitStruct(Padding(1),
@@ -242,17 +279,23 @@ class EF_USIM_AUTH_KEY(TransparentEF):
def _encode_bin(self, abstract_data: dict) -> bytearray:
if abstract_data['cfg']['algorithm'] == 'tuak':
- return self._constr_tuak.build(abstract_data)
+ return build_construct(self._constr_tuak, abstract_data)
else:
- return self._construct.build(abstract_data)
+ return build_construct(self._construct, abstract_data)
class EF_USIM_AUTH_KEY_2G(TransparentEF):
+ _test_de_encode = [
+ ( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ {"cfg": {"only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True,
+ "algorithm": "milenage"}, "key": "000102030405060708090a0b0c0d0e0f", "op_opc":
+ "101112131415161718191a1b1c1d1e1f"} ),
+ ]
def __init__(self, fid='af22', name='EF.USIM_AUTH_KEY_2G'):
super().__init__(fid, name=name, desc='USIM authentication key in 2G context')
- CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Bit,
- 'use_sres_deriv_func_2_in_3g'/Bit,
- 'use_opc_instead_of_op'/Bit,
+ CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Flag,
+ 'sres_deriv_func_in_2g'/Mapping(Bit, {1:0, 2:1}),
+ 'use_opc_instead_of_op'/Flag,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3, xor=14))
self._construct = Struct('cfg'/CfgByte,
'key'/HexAdapter(Bytes(16)),
diff --git a/pySim/tlv.py b/pySim/tlv.py
index bd95505..8bf43c6 100644
--- a/pySim/tlv.py
+++ b/pySim/tlv.py
@@ -16,22 +16,18 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-from typing import Optional, List, Dict, Any, Tuple
-from bidict import bidict
-from construct import *
+import inspect
+import abc
+import re
+from typing import List, Tuple
from pySim.utils import bertlv_encode_len, bertlv_parse_len, bertlv_encode_tag, bertlv_parse_tag
from pySim.utils import comprehensiontlv_encode_tag, comprehensiontlv_parse_tag
-from pySim.utils import bertlv_parse_one, comprehensiontlv_parse_one
from pySim.utils import bertlv_parse_tag_raw, comprehensiontlv_parse_tag_raw
+from pySim.utils import dgi_parse_tag_raw, dgi_parse_len, dgi_encode_tag, dgi_encode_len
-from pySim.construct import parse_construct, LV, HexAdapter, BcdAdapter, BitsRFU, GsmStringAdapter
-from pySim.exceptions import *
+from pySim.construct import build_construct, parse_construct
-import inspect
-import abc
-import re
def camel_to_snake(name):
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
@@ -41,9 +37,9 @@ class TlvMeta(abc.ABCMeta):
"""Metaclass which we use to set some class variables at the time of defining a subclass.
This allows us to create subclasses for each TLV/IE type, where the class represents fixed
parameters like the tag/type and instances of it represent the actual TLV data."""
- def __new__(metacls, name, bases, namespace, **kwargs):
- #print("TlvMeta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
- x = super().__new__(metacls, name, bases, namespace)
+ def __new__(mcs, name, bases, namespace, **kwargs):
+ #print("TlvMeta_new_(mcs=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (mcs, name, bases, namespace, kwargs))
+ x = super().__new__(mcs, name, bases, namespace)
# this becomes a _class_ variable, not an instance variable
x.tag = namespace.get('tag', kwargs.get('tag', None))
x.desc = namespace.get('desc', kwargs.get('desc', None))
@@ -64,9 +60,9 @@ class TlvCollectionMeta(abc.ABCMeta):
"""Metaclass which we use to set some class variables at the time of defining a subclass.
This allows us to create subclasses for each Collection type, where the class represents fixed
parameters like the nested IE classes and instances of it represent the actual TLV data."""
- def __new__(metacls, name, bases, namespace, **kwargs):
- #print("TlvCollectionMeta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
- x = super().__new__(metacls, name, bases, namespace)
+ def __new__(mcs, name, bases, namespace, **kwargs):
+ #print("TlvCollectionMeta_new_(mcs=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (mcs, name, bases, namespace, kwargs))
+ x = super().__new__(mcs, name, bases, namespace)
# this becomes a _class_ variable, not an instance variable
x.possible_nested = namespace.get('nested', kwargs.get('nested', None))
return x
@@ -84,15 +80,15 @@ class Transcodable(abc.ABC):
self.decoded = None
self._construct = None
- def to_bytes(self) -> bytes:
+ def to_bytes(self, context: dict = {}) -> bytes:
"""Convert from internal representation to binary bytes. Store the binary result
in the internal state and return it."""
- if not self.decoded:
+ if self.decoded is None:
do = b''
elif self._construct:
- do = self._construct.build(self.decoded, total_len=None)
+ do = build_construct(self._construct, self.decoded, context)
elif self.__class__._construct:
- do = self.__class__._construct.build(self.decoded, total_len=None)
+ do = build_construct(self.__class__._construct, self.decoded, context)
else:
do = self._to_bytes()
self.encoded = do
@@ -102,16 +98,16 @@ class Transcodable(abc.ABC):
def _to_bytes(self):
raise NotImplementedError('%s._to_bytes' % type(self).__name__)
- def from_bytes(self, do: bytes):
+ def from_bytes(self, do: bytes, context: dict = {}):
"""Convert from binary bytes to internal representation. Store the decoded result
in the internal state and return it."""
self.encoded = do
if self.encoded == b'':
self.decoded = None
elif self._construct:
- self.decoded = parse_construct(self._construct, do)
+ self.decoded = parse_construct(self._construct, do, context=context)
elif self.__class__._construct:
- self.decoded = parse_construct(self.__class__._construct, do)
+ self.decoded = parse_construct(self.__class__._construct, do, context=context)
else:
self.decoded = self._from_bytes(do)
return self.decoded
@@ -157,52 +153,46 @@ class IE(Transcodable, metaclass=TlvMeta):
def from_dict(self, decoded: dict):
"""Set the IE internal decoded representation to data from the argument.
If this is a nested IE, the child IE instance list is re-created."""
+ expected_key_name = camel_to_snake(type(self).__name__)
+ if not expected_key_name in decoded:
+ raise ValueError("Dict %s doesn't contain expected key %s" % (decoded, expected_key_name))
if self.nested_collection:
- self.children = self.nested_collection.from_dict(decoded)
+ self.children = self.nested_collection.from_dict(decoded[expected_key_name])
else:
self.children = []
- expected_key_name = camel_to_snake(type(self).__name__)
- if not expected_key_name in decoded:
- raise ValueError("Dict %s doesn't contain expected key %s" % (decoded, expected_key_name))
self.decoded = decoded[expected_key_name]
def is_constructed(self):
"""Is this IE constructed by further nested IEs?"""
- if len(self.children):
- return True
- else:
- return False
+ return bool(len(self.children) > 0)
@abc.abstractmethod
- def to_ie(self) -> bytes:
+ def to_ie(self, context: dict = {}) -> bytes:
"""Convert the internal representation to entire IE including IE header."""
- def to_bytes(self) -> bytes:
- """Convert the internal representation _of the value part_ to binary bytes."""
+ def to_bytes(self, context: dict = {}) -> bytes:
+ """Convert the internal representation *of the value part* to binary bytes."""
if self.is_constructed():
# concatenate the encoded IE of all children to form the value part
out = b''
for c in self.children:
- out += c.to_ie()
+ out += c.to_ie(context=context)
return out
else:
- return super().to_bytes()
+ return super().to_bytes(context=context)
- def from_bytes(self, do: bytes):
- """Parse _the value part_ from binary bytes to internal representation."""
+ def from_bytes(self, do: bytes, context: dict = {}):
+ """Parse *the value part* from binary bytes to internal representation."""
if self.nested_collection:
- self.children = self.nested_collection.from_bytes(do)
+ self.children = self.nested_collection.from_bytes(do, context=context)
else:
self.children = []
- return super().from_bytes(do)
+ return super().from_bytes(do, context=context)
class TLV_IE(IE):
"""Abstract base class for various TLV type Information Elements."""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
-
def _compute_tag(self) -> int:
"""Compute the tag (sometimes the tag encodes part of the value)."""
return self.tag
@@ -226,15 +216,15 @@ class TLV_IE(IE):
"""Encode the length part assuming a certain binary value. Must be provided by
derived (TLV format specific) class."""
- def to_ie(self):
- return self.to_tlv()
+ def to_ie(self, context: dict = {}):
+ return self.to_tlv(context=context)
- def to_tlv(self):
+ def to_tlv(self, context: dict = {}):
"""Convert the internal representation to binary TLV bytes."""
- val = self.to_bytes()
+ val = self.to_bytes(context=context)
return self._encode_tag() + self._encode_len(val) + val
- def from_tlv(self, do: bytes):
+ def from_tlv(self, do: bytes, context: dict = {}):
if len(do) == 0:
return {}, b''
(rawtag, remainder) = self.__class__._parse_tag_raw(do)
@@ -248,16 +238,13 @@ class TLV_IE(IE):
else:
value = do
remainder = b''
- dec = self.from_bytes(value)
+ dec = self.from_bytes(value, context=context)
return dec, remainder
class BER_TLV_IE(TLV_IE):
"""TLV_IE formatted as ASN.1 BER described in ITU-T X.690 8.1.2."""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
-
@classmethod
def _decode_tag(cls, do: bytes) -> Tuple[dict, bytes]:
return bertlv_parse_tag(do)
@@ -303,6 +290,24 @@ class COMPR_TLV_IE(TLV_IE):
return bertlv_encode_len(len(val))
+class DGI_TLV_IE(TLV_IE):
+ """TLV_IE formated as GlobalPlatform Systems Scripting Language Specification v1.1.0 Annex B."""
+
+ @classmethod
+ def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
+ return dgi_parse_tag_raw(do)
+
+ @classmethod
+ def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
+ return dgi_parse_len(do)
+
+ def _encode_tag(self) -> bytes:
+ return dgi_encode_tag(self._compute_tag())
+
+ def _encode_len(self, val: bytes) -> bytes:
+ return dgi_encode_len(len(val))
+
+
class TLV_IE_Collection(metaclass=TlvCollectionMeta):
# we specify the metaclass so any downstream subclasses will automatically use it
"""A TLV_IE_Collection consists of multiple TLV_IE classes identified by their tags.
@@ -318,7 +323,7 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
self.members_by_tag = {}
self.members_by_name = {}
self.members_by_tag = {m.tag: m for m in self.members}
- self.members_by_name = {m.__name__: m for m in self.members}
+ self.members_by_name = {camel_to_snake(m.__name__): m for m in self.members}
# if we are a constructed IE, [ordered] list of actual child-IE instances
self.children = kwargs.get('children', [])
self.encoded = None
@@ -343,7 +348,7 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
else:
raise TypeError
- def from_bytes(self, binary: bytes) -> List[TLV_IE]:
+ def from_bytes(self, binary: bytes, context: dict = {}) -> List[TLV_IE]:
"""Create a list of TLV_IEs from the collection based on binary input data.
Args:
binary : binary bytes of encoded data
@@ -357,15 +362,16 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
first = next(iter(self.members_by_tag.values()))
# iterate until no binary trailer is left
while len(remainder):
+ context['siblings'] = res
# obtain the tag at the start of the remainder
- tag, r = first._parse_tag_raw(remainder)
- if tag == None:
- return res
+ tag, _r = first._parse_tag_raw(remainder)
+ if tag is None:
+ break
if tag in self.members_by_tag:
cls = self.members_by_tag[tag]
# create an instance and parse accordingly
inst = cls()
- dec, remainder = inst.from_tlv(remainder)
+ _dec, remainder = inst.from_tlv(remainder, context=context)
res.append(inst)
else:
# unknown tag; create the related class on-the-fly using the same base class
@@ -376,7 +382,7 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
cls._to_bytes = lambda s: bytes.fromhex(s.decoded['raw'])
# create an instance and parse accordingly
inst = cls()
- dec, remainder = inst.from_tlv(remainder)
+ _dec, remainder = inst.from_tlv(remainder, context=context)
res.append(inst)
self.children = res
return res
@@ -386,33 +392,45 @@ class TLV_IE_Collection(metaclass=TlvCollectionMeta):
of dicts, where they key indicates the name of the TLV_IE subclass to use."""
# list of instances of TLV_IE collection member classes appearing in the data
res = []
+ # iterate over members of the list passed into "decoded"
for i in decoded:
+ # iterate over all the keys (typically one!) within the current list item dict
for k in i.keys():
+ # check if we have a member identified by the dict key
if k in self.members_by_name:
+ # resolve the class for that name; create an instance of it
cls = self.members_by_name[k]
inst = cls()
- inst.from_dict({k: i[k]})
+ if cls.nested_collection_cls:
+ # in case of collections, we want to pass the raw "value" portion to from_dict,
+ # as to_dict() below intentionally omits the collection-class-name as key
+ inst.from_dict(i[k])
+ else:
+ inst.from_dict({k: i[k]})
res.append(inst)
else:
raise ValueError('%s: Unknown TLV Class %s in %s; expected %s' %
- (self, i[0], decoded, self.members_by_name.keys()))
+ (self, k, decoded, self.members_by_name.keys()))
self.children = res
return res
def to_dict(self):
+ # we intentionally return not a dict, but a list of dicts. We could prefix by
+ # self.__class__.__name__, but that is usually some meaningless auto-generated collection name.
return [x.to_dict() for x in self.children]
- def to_bytes(self):
+ def to_bytes(self, context: dict = {}):
out = b''
+ context['siblings'] = self.children
for c in self.children:
- out += c.to_tlv()
+ out += c.to_tlv(context=context)
return out
- def from_tlv(self, do):
- return self.from_bytes(do)
+ def from_tlv(self, do, context: dict = {}):
+ return self.from_bytes(do, context=context)
- def to_tlv(self):
- return self.to_bytes()
+ def to_tlv(self, context: dict = {}):
+ return self.to_bytes(context=context)
def flatten_dict_lists(inp):
@@ -424,8 +442,12 @@ def flatten_dict_lists(inp):
return False
return True
+ def are_elements_unique(lod):
+ set_of_keys = {list(x.keys())[0] for x in lod}
+ return len(lod) == len(set_of_keys)
+
if isinstance(inp, list):
- if are_all_elements_dict(inp):
+ if are_all_elements_dict(inp) and are_elements_unique(inp):
# flatten into one shared dict
newdict = {}
for e in inp:
@@ -433,10 +455,10 @@ def flatten_dict_lists(inp):
newdict[key] = e[key]
inp = newdict
# process result as any native dict
- return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
+ return {k:flatten_dict_lists(v) for k,v in inp.items()}
else:
return [flatten_dict_lists(x) for x in inp]
elif isinstance(inp, dict):
- return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
+ return {k:flatten_dict_lists(v) for k,v in inp.items()}
else:
return inp
diff --git a/pySim/transport/__init__.py b/pySim/transport/__init__.py
index 09752ac..dd06af8 100644
--- a/pySim/transport/__init__.py
+++ b/pySim/transport/__init__.py
@@ -3,18 +3,19 @@
""" pySim: PCSC reader transport link base
"""
+import os
import abc
import argparse
from typing import Optional, Tuple
+from construct import Construct
from pySim.exceptions import *
-from pySim.construct import filter_dict
-from pySim.utils import sw_match, b2h, h2b, i2h, Hexstr
+from pySim.utils import sw_match, b2h, h2b, i2h, Hexstr, SwHexstr, SwMatchstr, ResTuple
from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
-# Copyright (C) 2021-2022 Harald Welte <laforge@osmocom.org>
+# Copyright (C) 2021-2023 Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -60,14 +61,18 @@ class ProactiveHandler(abc.ABC):
class LinkBase(abc.ABC):
"""Base class for link/transport to card."""
- def __init__(self, sw_interpreter=None, apdu_tracer=None,
+ def __init__(self, sw_interpreter=None, apdu_tracer: Optional[ApduTracer]=None,
proactive_handler: Optional[ProactiveHandler]=None):
self.sw_interpreter = sw_interpreter
self.apdu_tracer = apdu_tracer
self.proactive_handler = proactive_handler
@abc.abstractmethod
- def _send_apdu_raw(self, pdu: str) -> Tuple[str, str]:
+ def __str__(self) -> str:
+ """Implementation specific method for printing an information to identify the device."""
+
+ @abc.abstractmethod
+ def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
"""Implementation specific method for sending the PDU."""
def set_sw_interpreter(self, interp):
@@ -75,7 +80,7 @@ class LinkBase(abc.ABC):
self.sw_interpreter = interp
@abc.abstractmethod
- def wait_for_card(self, timeout: int = None, newcardonly: bool = False):
+ def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
"""Wait for a card and connect to it
Args:
@@ -98,7 +103,7 @@ class LinkBase(abc.ABC):
"""Resets the card (power down/up)
"""
- def send_apdu_raw(self, pdu: str):
+ def send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU with minimal processing
Args:
@@ -115,7 +120,7 @@ class LinkBase(abc.ABC):
self.apdu_tracer.trace_response(pdu, sw, data)
return (data, sw)
- def send_apdu(self, pdu):
+ def send_apdu(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU and auto fetch response data
Args:
@@ -131,12 +136,13 @@ class LinkBase(abc.ABC):
# 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:
- if (sw is not None):
- if ((sw[0:2] == '9f') or (sw[0:2] == '61')):
+ if sw is not None:
+ while ((sw[0:2] == '9f') or (sw[0:2] == '61')):
# 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
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
- data, sw = self.send_apdu_raw(pdu_gr)
+ d, sw = self.send_apdu_raw(pdu_gr)
+ data += d
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
pdu_gr = pdu[0:8] + sw[2:4]
@@ -144,7 +150,7 @@ class LinkBase(abc.ABC):
return data, sw
- def send_apdu_checksw(self, pdu, sw="9000"):
+ def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""Sends an APDU and check returned SW
Args:
@@ -212,108 +218,42 @@ class LinkBase(abc.ABC):
raise SwMatchError(rv[1], sw.lower(), self.sw_interpreter)
return rv
- def send_apdu_constr(self, cla, ins, p1, p2, cmd_constr, cmd_data, resp_constr):
- """Build and sends an APDU using a 'construct' definition; parses response.
-
- Args:
- cla : string (in hex) ISO 7816 class byte
- ins : string (in hex) ISO 7816 instruction byte
- p1 : string (in hex) ISO 7116 Parameter 1 byte
- p2 : string (in hex) ISO 7116 Parameter 2 byte
- cmd_cosntr : defining how to generate binary APDU command data
- cmd_data : command data passed to cmd_constr
- resp_cosntr : defining how to decode binary APDU response data
- Returns:
- Tuple of (decoded_data, sw)
- """
- cmd = cmd_constr.build(cmd_data) if cmd_data else ''
- p3 = i2h([len(cmd)])
- pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)])
- (data, sw) = self.send_apdu(pdu)
- if data:
- # filter the resulting dict to avoid '_io' members inside
- rsp = filter_dict(resp_constr.parse(h2b(data)))
- else:
- rsp = None
- return (rsp, sw)
-
- def send_apdu_constr_checksw(self, cla, ins, p1, p2, cmd_constr, cmd_data, resp_constr,
- sw_exp="9000"):
- """Build and sends an APDU using a 'construct' definition; parses response.
-
- Args:
- cla : string (in hex) ISO 7816 class byte
- ins : string (in hex) ISO 7816 instruction byte
- p1 : string (in hex) ISO 7116 Parameter 1 byte
- p2 : string (in hex) ISO 7116 Parameter 2 byte
- cmd_cosntr : defining how to generate binary APDU command data
- cmd_data : command data passed to cmd_constr
- resp_cosntr : defining how to decode binary APDU response data
- exp_sw : string (in hex) of status word (ex. "9000")
- Returns:
- Tuple of (decoded_data, sw)
- """
- (rsp, sw) = self.send_apdu_constr(cla, ins,
- p1, p2, cmd_constr, cmd_data, resp_constr)
- if not sw_match(sw, sw_exp):
- raise SwMatchError(sw, sw_exp.lower(), self.sw_interpreter)
- return (rsp, sw)
-
-
-def argparse_add_reader_args(arg_parser):
+def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
"""Add all reader related arguments to the given argparse.Argumentparser instance."""
- serial_group = arg_parser.add_argument_group('Serial Reader')
- serial_group.add_argument('-d', '--device', metavar='DEV', default='/dev/ttyUSB0',
- help='Serial Device for SIM access')
- serial_group.add_argument('-b', '--baud', dest='baudrate', type=int, metavar='BAUD', default=9600,
- help='Baud rate used for SIM access')
-
- pcsc_group = arg_parser.add_argument_group('PC/SC Reader')
- pcsc_group.add_argument('-p', '--pcsc-device', type=int, dest='pcsc_dev', metavar='PCSC', default=None,
- help='PC/SC reader number to use for SIM access')
-
- modem_group = arg_parser.add_argument_group('AT Command Modem Reader')
- modem_group.add_argument('--modem-device', dest='modem_dev', metavar='DEV', default=None,
- help='Serial port of modem for Generic SIM Access (3GPP TS 27.007)')
- modem_group.add_argument('--modem-baud', type=int, metavar='BAUD', default=115200,
- help='Baud rate used for modem port')
-
- osmobb_group = arg_parser.add_argument_group('OsmocomBB Reader')
- osmobb_group.add_argument('--osmocon', dest='osmocon_sock', metavar='PATH', default=None,
- help='Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)')
+ from pySim.transport.serial import SerialSimLink
+ from pySim.transport.pcsc import PcscSimLink
+ from pySim.transport.modem_atcmd import ModemATCommandLink
+ from pySim.transport.calypso import CalypsoSimLink
+
+ SerialSimLink.argparse_add_reader_args(arg_parser)
+ PcscSimLink.argparse_add_reader_args(arg_parser)
+ ModemATCommandLink.argparse_add_reader_args(arg_parser)
+ CalypsoSimLink.argparse_add_reader_args(arg_parser)
return arg_parser
-def init_reader(opts, **kwargs) -> Optional[LinkBase]:
+def init_reader(opts, **kwargs) -> LinkBase:
"""
Init card reader driver
"""
- sl = None # type : :Optional[LinkBase]
- try:
- if opts.pcsc_dev is not None:
- print("Using PC/SC reader interface")
- from pySim.transport.pcsc import PcscSimLink
- sl = PcscSimLink(opts.pcsc_dev, **kwargs)
- elif opts.osmocon_sock is not None:
- print("Using Calypso-based (OsmocomBB) reader interface")
- from pySim.transport.calypso import CalypsoSimLink
- sl = CalypsoSimLink(sock_path=opts.osmocon_sock, **kwargs)
- elif opts.modem_dev is not None:
- print("Using modem for Generic SIM Access (3GPP TS 27.007)")
- from pySim.transport.modem_atcmd import ModemATCommandLink
- sl = ModemATCommandLink(
- device=opts.modem_dev, baudrate=opts.modem_baud, **kwargs)
- else: # Serial reader is default
- print("Using serial reader interface")
- from pySim.transport.serial import SerialSimLink
- sl = SerialSimLink(device=opts.device,
- baudrate=opts.baudrate, **kwargs)
- return sl
- except Exception as e:
- if str(e):
- print("Card reader initialization failed with exception:\n" + str(e))
- else:
- print(
- "Card reader initialization failed with an exception of type:\n" + str(type(e)))
- return None
+ if opts.pcsc_dev is not None or opts.pcsc_regex is not None:
+ from pySim.transport.pcsc import PcscSimLink
+ sl = PcscSimLink(opts, **kwargs)
+ elif opts.osmocon_sock is not None:
+ from pySim.transport.calypso import CalypsoSimLink
+ sl = CalypsoSimLink(opts, **kwargs)
+ elif opts.modem_dev is not None:
+ from pySim.transport.modem_atcmd import ModemATCommandLink
+ sl = ModemATCommandLink(opts, **kwargs)
+ else: # Serial reader is default
+ print("No reader/driver specified; falling back to default (Serial reader)")
+ from pySim.transport.serial import SerialSimLink
+ sl = SerialSimLink(opts, **kwargs)
+
+ if os.environ.get('PYSIM_INTEGRATION_TEST') == "1":
+ print("Using %s reader interface" % (sl.name))
+ else:
+ print("Using reader %s" % sl)
+
+ return sl
diff --git a/pySim/transport/calypso.py b/pySim/transport/calypso.py
index e1326ac..6252137 100644
--- a/pySim/transport/calypso.py
+++ b/pySim/transport/calypso.py
@@ -20,10 +20,12 @@ import select
import struct
import socket
import os
+import argparse
+from typing import Optional
from pySim.transport import LinkBase
-from pySim.exceptions import *
-from pySim.utils import h2b, b2h
+from pySim.exceptions import ReaderError, ProtocolError
+from pySim.utils import h2b, b2h, Hexstr, ResTuple
class L1CTLMessage:
@@ -56,9 +58,9 @@ class L1CTLMessageReset(L1CTLMessage):
L1CTL_RES_T_FULL = 0x01
L1CTL_RES_T_SCHED = 0x02
- def __init__(self, type=L1CTL_RES_T_FULL):
- super(L1CTLMessageReset, self).__init__(self.L1CTL_RESET_REQ)
- self.data += struct.pack("Bxxx", type)
+ def __init__(self, ttype=L1CTL_RES_T_FULL):
+ super().__init__(self.L1CTL_RESET_REQ)
+ self.data += struct.pack("Bxxx", ttype)
class L1CTLMessageSIM(L1CTLMessage):
@@ -68,14 +70,16 @@ class L1CTLMessageSIM(L1CTLMessage):
L1CTL_SIM_CONF = 0x17
def __init__(self, pdu):
- super(L1CTLMessageSIM, self).__init__(self.L1CTL_SIM_REQ)
+ super().__init__(self.L1CTL_SIM_REQ)
self.data += pdu
class CalypsoSimLink(LinkBase):
"""Transport Link for Calypso based phones."""
+ name = 'Calypso-based (OsmocomBB) reader'
- def __init__(self, sock_path: str = "/tmp/osmocom_l2", **kwargs):
+ def __init__(self, opts: argparse.Namespace = argparse.Namespace(osmocon_sock="/tmp/osmocom_l2"), **kwargs):
+ sock_path = opts.osmocon_sock
super().__init__(**kwargs)
# Make sure that a given socket path exists
if not os.path.exists(sock_path):
@@ -88,10 +92,13 @@ class CalypsoSimLink(LinkBase):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(sock_path)
+ # Remember socket path
+ self._sock_path = sock_path
+
def __del__(self):
self.sock.close()
- def wait_for_rsp(self, exp_len=128):
+ def wait_for_rsp(self, exp_len: int = 128):
# Wait for incoming data (timeout is 3 seconds)
s, _, _ = select.select([self.sock], [], [], 3.0)
if not s:
@@ -118,10 +125,10 @@ class CalypsoSimLink(LinkBase):
def disconnect(self):
pass # Nothing to do really ...
- def wait_for_card(self, timeout=None, newcardonly=False):
+ def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
pass # Nothing to do really ...
- def _send_apdu_raw(self, pdu):
+ def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
# Request FULL reset
req_msg = L1CTLMessageSIM(h2b(pdu))
@@ -154,3 +161,15 @@ class CalypsoSimLink(LinkBase):
sw = rsp[-2:]
return b2h(data), b2h(sw)
+
+ def __str__(self) -> str:
+ return "osmocon:%s" % (self._sock_path)
+
+ @staticmethod
+ def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
+ osmobb_group = arg_parser.add_argument_group('OsmocomBB Reader', """Use an OsmocomBB compatible phone
+to access the SIM inserted to the phone SIM slot. This will require you to run the OsmocomBB firmware inside
+the phone (can be ram-loaded). It also requires that you run the ``osmocon`` program, which provides a unix
+domain socket to which this reader driver can attach.""")
+ osmobb_group.add_argument('--osmocon', dest='osmocon_sock', metavar='PATH', default=None,
+ help='Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)')
diff --git a/pySim/transport/modem_atcmd.py b/pySim/transport/modem_atcmd.py
index ea50bc9..5943e3a 100644
--- a/pySim/transport/modem_atcmd.py
+++ b/pySim/transport/modem_atcmd.py
@@ -17,12 +17,15 @@
#
import logging as log
-import serial
import time
import re
+import argparse
+from typing import Optional
+import serial
+from pySim.utils import Hexstr, ResTuple
from pySim.transport import LinkBase
-from pySim.exceptions import *
+from pySim.exceptions import ReaderError, ProtocolError
# HACK: if somebody needs to debug this thing
# log.root.setLevel(log.DEBUG)
@@ -30,8 +33,12 @@ from pySim.exceptions import *
class ModemATCommandLink(LinkBase):
"""Transport Link for 3GPP TS 27.007 compliant modems."""
+ name = "modem for Generic SIM Access (3GPP TS 27.007)"
- def __init__(self, device: str = '/dev/ttyUSB0', baudrate: int = 115200, **kwargs):
+ def __init__(self, opts: argparse.Namespace = argparse.Namespace(modem_dev='/dev/ttyUSB0',
+ modem_baud=115200), **kwargs):
+ device = opts.modem_dev
+ baudrate = opts.modem_baud
super().__init__(**kwargs)
self._sl = serial.Serial(device, baudrate, timeout=5)
self._echo = False # this will be auto-detected by _check_echo()
@@ -50,7 +57,7 @@ class ModemATCommandLink(LinkBase):
def send_at_cmd(self, cmd, timeout=0.2, patience=0.002):
# Convert from string to bytes, if needed
- bcmd = cmd if type(cmd) is bytes else cmd.encode()
+ bcmd = cmd if isinstance(cmd, bytes) else cmd.encode()
bcmd += b'\r'
# Clean input buffer from previous/unexpected data
@@ -60,9 +67,9 @@ class ModemATCommandLink(LinkBase):
log.debug('Sending AT command: %s', cmd)
try:
wlen = self._sl.write(bcmd)
- assert(wlen == len(bcmd))
- except:
- raise ReaderError('Failed to send AT command: %s' % cmd)
+ assert wlen == len(bcmd)
+ except Exception as exc:
+ raise ReaderError('Failed to send AT command: %s' % cmd) from exc
rsp = b''
its = 1
@@ -84,8 +91,7 @@ class ModemATCommandLink(LinkBase):
break
time.sleep(patience)
its += 1
- log.debug('Command took %0.6fs (%d cycles a %fs)',
- time.time() - t_start, its, patience)
+ log.debug('Command took %0.6fs (%d cycles a %fs)', time.time() - t_start, its, patience)
if self._echo:
# Skip echo chars
@@ -113,11 +119,10 @@ class ModemATCommandLink(LinkBase):
if result[-1] == b'OK':
self._echo = False
return
- elif result[-1] == b'AT\r\r\nOK':
+ if result[-1] == b'AT\r\r\nOK':
self._echo = True
return
- raise ReaderError(
- 'Interface \'%s\' does not respond to \'AT\' command' % self._device)
+ raise ReaderError('Interface \'%s\' does not respond to \'AT\' command' % self._device)
def reset_card(self):
# Reset the modem, just to be sure
@@ -128,7 +133,7 @@ class ModemATCommandLink(LinkBase):
if self.send_at_cmd('AT+CSIM=?') != [b'OK']:
raise ReaderError('The modem does not seem to support SIM access')
- log.info('Modem at \'%s\' is ready!' % self._device)
+ log.info('Modem at \'%s\' is ready!', self._device)
def connect(self):
pass # Nothing to do really ...
@@ -136,10 +141,10 @@ class ModemATCommandLink(LinkBase):
def disconnect(self):
pass # Nothing to do really ...
- def wait_for_card(self, timeout=None, newcardonly=False):
+ def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
pass # Nothing to do really ...
- def _send_apdu_raw(self, pdu):
+ def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
# Make sure pdu has upper case hex digits [A-F]
pdu = pdu.upper()
@@ -158,12 +163,25 @@ class ModemATCommandLink(LinkBase):
# Make sure that the response has format: b'+CSIM: %d,\"%s\"'
try:
result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)
- (rsp_pdu_len, rsp_pdu) = result.groups()
- except:
- raise ReaderError('Failed to parse response from modem: %s' % rsp)
+ (_rsp_pdu_len, rsp_pdu) = result.groups()
+ except Exception as exc:
+ raise ReaderError('Failed to parse response from modem: %s' % rsp) from exc
# TODO: make sure we have at least SW
data = rsp_pdu[:-4].decode().lower()
sw = rsp_pdu[-4:].decode().lower()
log.debug('Command response: %s, %s', data, sw)
return data, sw
+
+ def __str__(self) -> str:
+ return "modem:%s" % self._device
+
+ @staticmethod
+ def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
+ modem_group = arg_parser.add_argument_group('AT Command Modem Reader', """Talk to a SIM Card inside a
+mobile phone or cellular modem which is attached to this computer and offers an AT command interface including
+the AT+CSIM interface for Generic SIM access as specified in 3GPP TS 27.007.""")
+ modem_group.add_argument('--modem-device', dest='modem_dev', metavar='DEV', default=None,
+ help='Serial port of modem for Generic SIM Access (3GPP TS 27.007)')
+ modem_group.add_argument('--modem-baud', type=int, metavar='BAUD', default=115200,
+ help='Baud rate used for modem port')
diff --git a/pySim/transport/pcsc.py b/pySim/transport/pcsc.py
index e3f2546..8a1e03c 100644
--- a/pySim/transport/pcsc.py
+++ b/pySim/transport/pcsc.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
-# Copyright (C) 2010 Harald Welte <laforge@gnumonks.org>
+# Copyright (C) 2010-2023 Harald Welte <laforge@gnumonks.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,26 +17,48 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
+import argparse
+import re
+from typing import Optional
+
from smartcard.CardConnection import CardConnection
from smartcard.CardRequest import CardRequest
-from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException, CardConnectionException
+from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException
from smartcard.System import readers
+from smartcard.ExclusiveConnectCardConnection import ExclusiveConnectCardConnection
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
from pySim.transport import LinkBase
-from pySim.utils import h2i, i2h
+from pySim.utils import h2i, i2h, Hexstr, ResTuple
class PcscSimLink(LinkBase):
""" pySim: PCSC reader transport link."""
+ name = 'PC/SC'
- def __init__(self, reader_number: int = 0, **kwargs):
+ def __init__(self, opts: argparse.Namespace = argparse.Namespace(pcsc_dev=0), **kwargs):
super().__init__(**kwargs)
+ self._reader = None
r = readers()
- if reader_number >= len(r):
- raise ReaderError('No reader found for number %d' % reader_number)
- self._reader = r[reader_number]
+ if opts.pcsc_dev is not None:
+ # actual reader index number (integer)
+ reader_number = opts.pcsc_dev
+ if reader_number >= len(r):
+ raise ReaderError('No reader found for number %d' % reader_number)
+ self._reader = r[reader_number]
+ else:
+ # reader regex string
+ cre = re.compile(opts.pcsc_regex)
+ for reader in r:
+ if cre.search(reader.name):
+ self._reader = reader
+ break
+ if not self._reader:
+ raise ReaderError('No matching reader found for regex %s' % opts.pcsc_regex)
+
self._con = self._reader.createConnection()
+ if not getattr(opts, "pcsc_shared", False):
+ self._con = ExclusiveConnectCardConnection(self._con)
def __del__(self):
try:
@@ -44,15 +66,14 @@ class PcscSimLink(LinkBase):
self._con.disconnect()
except:
pass
- return
- def wait_for_card(self, timeout: int = None, newcardonly: bool = False):
+ def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
cr = CardRequest(readers=[self._reader],
timeout=timeout, newcardonly=newcardonly)
try:
cr.waitforcard()
- except CardRequestTimeoutException:
- raise NoCardError()
+ except CardRequestTimeoutException as exc:
+ raise NoCardError() from exc
self.connect()
def connect(self):
@@ -63,12 +84,12 @@ class PcscSimLink(LinkBase):
# Explicitly select T=0 communication protocol
self._con.connect(CardConnection.T0_protocol)
- except CardConnectionException:
- raise ProtocolError()
- except NoCardException:
- raise NoCardError()
+ except CardConnectionException as exc:
+ raise ProtocolError() from exc
+ except NoCardException as exc:
+ raise NoCardError() from exc
- def get_atr(self):
+ def get_atr(self) -> Hexstr:
return self._con.getATR()
def disconnect(self):
@@ -79,7 +100,7 @@ class PcscSimLink(LinkBase):
self.connect()
return 1
- def _send_apdu_raw(self, pdu):
+ def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
apdu = h2i(pdu)
@@ -89,3 +110,22 @@ class PcscSimLink(LinkBase):
# Return value
return i2h(data), i2h(sw)
+
+ def __str__(self) -> str:
+ return "PCSC[%s]" % (self._reader)
+
+ @staticmethod
+ def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
+ pcsc_group = arg_parser.add_argument_group('PC/SC Reader',
+ """Use a PC/SC card reader to talk to the SIM card. PC/SC is a standard API for how applications
+access smart card readers, and is available on a variety of operating systems, such as Microsoft
+Windows, MacOS X and Linux. Most vendors of smart card readers provide drivers that offer a PC/SC
+interface, if not even a generic USB CCID driver is used. You can use a tool like ``pcsc_scan -r``
+to obtain a list of readers available on your system. """)
+ pcsc_group.add_argument('--pcsc-shared', action='store_true',
+ help='Open PC/SC reaer in SHARED access (default: EXCLUSIVE)')
+ dev_group = pcsc_group.add_mutually_exclusive_group()
+ dev_group.add_argument('-p', '--pcsc-device', type=int, dest='pcsc_dev', metavar='PCSC', default=None,
+ help='Number of PC/SC reader to use for SIM access')
+ dev_group.add_argument('--pcsc-regex', type=str, dest='pcsc_regex', metavar='REGEX', default=None,
+ help='Regex matching PC/SC reader to use for SIM access')
diff --git a/pySim/transport/serial.py b/pySim/transport/serial.py
index 3313a5e..b4eddcd 100644
--- a/pySim/transport/serial.py
+++ b/pySim/transport/serial.py
@@ -16,42 +16,45 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-import serial
import time
-import os.path
+import os
+import argparse
+from typing import Optional
+import serial
from pySim.exceptions import NoCardError, ProtocolError
from pySim.transport import LinkBase
-from pySim.utils import h2b, b2h
+from pySim.utils import h2b, b2h, Hexstr, ResTuple
class SerialSimLink(LinkBase):
""" pySim: Transport Link for serial (RS232) based readers included with simcard"""
+ name = 'Serial'
- def __init__(self, device: str = '/dev/ttyUSB0', baudrate: int = 9600, rst: str = '-rts',
+ def __init__(self, opts = argparse.Namespace(device='/dev/ttyUSB0', baudrate=9600), rst: str = '-rts',
debug: bool = False, **kwargs):
super().__init__(**kwargs)
- if not os.path.exists(device):
- raise ValueError("device file %s does not exist -- abort" % device)
+ if not os.path.exists(opts.device):
+ raise ValueError("device file %s does not exist -- abort" % opts.device)
self._sl = serial.Serial(
- port=device,
+ port=opts.device,
parity=serial.PARITY_EVEN,
bytesize=serial.EIGHTBITS,
stopbits=serial.STOPBITS_TWO,
timeout=1,
xonxoff=0,
rtscts=0,
- baudrate=baudrate,
+ baudrate=opts.baudrate,
)
self._rst_pin = rst
self._debug = debug
self._atr = None
def __del__(self):
- if (hasattr(self, "_sl")):
+ if hasattr(self, "_sl"):
self._sl.close()
- def wait_for_card(self, timeout=None, newcardonly=False):
+ def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
# Direct try
existing = False
@@ -59,8 +62,7 @@ class SerialSimLink(LinkBase):
self.reset_card()
if not newcardonly:
return
- else:
- existing = True
+ existing = True
except NoCardError:
pass
@@ -83,7 +85,7 @@ class SerialSimLink(LinkBase):
# Tolerate a couple of protocol error ... can happen if
# we try when the card is 'half' inserted
pe += 1
- if (pe > 2):
+ if pe > 2:
raise
# Timed out ...
@@ -92,7 +94,7 @@ class SerialSimLink(LinkBase):
def connect(self):
self.reset_card()
- def get_atr(self):
+ def get_atr(self) -> Hexstr:
return self._atr
def disconnect(self):
@@ -102,7 +104,7 @@ class SerialSimLink(LinkBase):
rv = self._reset_card()
if rv == 0:
raise NoCardError()
- elif rv < 0:
+ if rv < 0:
raise ProtocolError()
return rv
@@ -117,8 +119,8 @@ class SerialSimLink(LinkBase):
try:
rst_meth = rst_meth_map[self._rst_pin[1:]]
rst_val = rst_val_map[self._rst_pin[0]]
- except:
- raise ValueError('Invalid reset pin %s' % self._rst_pin)
+ except Exception as exc:
+ raise ValueError('Invalid reset pin %s' % self._rst_pin) from exc
rst_meth(rst_val)
time.sleep(0.1) # 100 ms
@@ -184,7 +186,7 @@ class SerialSimLink(LinkBase):
def _rx_byte(self):
return self._sl.read()
- def _send_apdu_raw(self, pdu):
+ def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
pdu = h2b(pdu)
data_len = pdu[4] # P3
@@ -200,7 +202,7 @@ class SerialSimLink(LinkBase):
b = self._rx_byte()
if ord(b) == pdu[1]:
break
- elif b != '\x60':
+ if b != '\x60':
# Ok, it 'could' be SW1
sw1 = b
sw2 = self._rx_byte()
@@ -219,7 +221,7 @@ class SerialSimLink(LinkBase):
to_recv = data_len - len(pdu) + 5 + 2
data = bytes(0)
- while (len(data) < to_recv):
+ while len(data) < to_recv:
b = self._rx_byte()
if (to_recv == 2) and (b == '\x60'): # Ignore NIL if we have no RX data (hack ?)
continue
@@ -235,3 +237,16 @@ class SerialSimLink(LinkBase):
# Return value
return b2h(data), b2h(sw)
+
+ def __str__(self) -> str:
+ return "serial:%s" % (self._sl.name)
+
+ @staticmethod
+ def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
+ serial_group = arg_parser.add_argument_group('Serial Reader', """Use a simple/ultra-low-cost serial reader
+attached to a (physical or USB/virtual) RS232 port. This doesn't work with all RS232-attached smart card
+readers, only with the very primitive readers following the ancient `Phoenix` or `Smart Mouse` design.""")
+ serial_group.add_argument('-d', '--device', metavar='DEV', default='/dev/ttyUSB0',
+ help='Serial Device for SIM access')
+ serial_group.add_argument('-b', '--baud', dest='baudrate', type=int, metavar='BAUD', default=9600,
+ help='Baud rate used for SIM access')
diff --git a/pySim/ts_102_221.py b/pySim/ts_102_221.py
index b6c003b..1b40b09 100644
--- a/pySim/ts_102_221.py
+++ b/pySim/ts_102_221.py
@@ -1,7 +1,7 @@
# coding=utf-8
"""Utilities / Functions related to ETSI TS 102 221, the core UICC spec.
-(C) 2021 by Harald Welte <laforge@osmocom.org>
+(C) 2021-2024 by Harald Welte <laforge@osmocom.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -16,22 +16,24 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
+from bidict import bidict
-from construct import *
+from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
from construct import Optional as COptional
+
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
-from bidict import bidict
from pySim.profile import CardProfile
from pySim.profile import match_uicc
-from pySim.profile import match_sim
-import pySim.iso7816_4 as iso7816_4
+from pySim import iso7816_4
# A UICC will usually also support 2G functionality. If this is the case, we
# need to add DF_GSM and DF_TELECOM along with the UICC related files
-from pySim.ts_51_011 import DF_GSM, DF_TELECOM
+from pySim.ts_51_011 import AddonSIM
+from pySim.gsm_r import AddonGSMR
+from pySim.cdma_ruim import AddonRUIM
ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [
# TS 102 221 Section 10.1.2 Table 10.5 "Coding of Instruction Byte"
@@ -78,6 +80,10 @@ ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [
CardCommand('RESIZE FILE', 0xD4, ['8X', 'CX']),
])
+
+# ETSI TS 102 221 6.2.1
+SupplyVoltageClasses = FlagsEnum(Int8ub, a=0x1, b=0x2, c=0x4, d=0x8, e=0x10)
+
# ETSI TS 102 221 11.1.1.4.2
class FileSize(BER_TLV_IE, tag=0x80):
_construct = GreedyInteger(minlen=2)
@@ -88,14 +94,24 @@ class TotalFileSize(BER_TLV_IE, tag=0x81):
# ETSI TS 102 221 11.1.1.4.3
class FileDescriptor(BER_TLV_IE, tag=0x82):
+ _test_decode = [
+ # FIXME: this doesn't work as _encode test for some strange reason.
+ ( '82027921', { "file_descriptor_byte": { "shareable": True, "structure": "ber_tlv" }, "record_len": None, "num_of_rec": None } ),
+ ]
+ _test_de_encode = [
+ ( '82027821', { "file_descriptor_byte": { "shareable": True, "file_type": "df", "structure": "no_info_given" }, "record_len": None, "num_of_rec": None }),
+ ( '82024121', { "file_descriptor_byte": { "shareable": True, "file_type": "working_ef", "structure": "transparent" }, "record_len": None, "num_of_rec": None } ),
+ ( '82054221006e05', { "file_descriptor_byte": { "shareable": True, "file_type": "working_ef", "structure": "linear_fixed" }, "record_len": 110, "num_of_rec": 5 } ),
+ ]
class BerTlvAdapter(Adapter):
def _parse(self, obj, context, path):
- if obj == 0x39:
+ data = obj.read()
+ if data == b'\x01\x01\x01\x00\x00\x01':
return 'ber_tlv'
raise ValidationError
def _build(self, obj, context, path):
if obj == 'ber_tlv':
- return 0x39
+ return b'\x01\x01\x01\x00\x00\x01'
raise ValidationError
FDB = Select(BitStruct(Const(0, Bit), 'shareable'/Flag, 'structure'/BerTlvAdapter(Const(0x39, BitsInteger(6)))),
@@ -119,7 +135,7 @@ class UiccCharacteristics(BER_TLV_IE, tag=0x80):
# ETSI TS 102 221 11.1.1.4.6.2
class ApplicationPowerConsumption(BER_TLV_IE, tag=0x81):
- _construct = Struct('voltage_class'/Int8ub,
+ _construct = Struct('voltage_class'/SupplyVoltageClasses,
'power_consumption_ma'/Int8ub,
'reference_freq_100k'/Int8ub)
@@ -198,6 +214,9 @@ class ShortFileIdentifier(BER_TLV_IE, tag=0x88):
# ETSI TS 102 221 11.1.1.4.9
class LifeCycleStatusInteger(BER_TLV_IE, tag=0x8A):
+ _test_de_encode = [
+ ( '8a0105', 'operational_activated' ),
+ ]
def _from_bytes(self, do: bytes):
lcsi = int.from_bytes(do, 'big')
if lcsi == 0x00:
@@ -219,20 +238,19 @@ class LifeCycleStatusInteger(BER_TLV_IE, tag=0x8A):
def _to_bytes(self):
if self.decoded == 'no_information':
return b'\x00'
- elif self.decoded == 'creation':
+ if self.decoded == 'creation':
return b'\x01'
- elif self.decoded == 'initialization':
+ if self.decoded == 'initialization':
return b'\x03'
- elif self.decoded == 'operational_activated':
+ if self.decoded == 'operational_activated':
return b'\x05'
- elif self.decoded == 'operational_deactivated':
+ if self.decoded == 'operational_deactivated':
return b'\x04'
- elif self.decoded == 'termination':
+ if self.decoded == 'termination':
return b'\x0c'
- elif isinstance(self.decoded, int):
+ if isinstance(self.decoded, int):
return self.decoded.to_bytes(1, 'big')
- else:
- raise ValueError
+ raise ValueError
# ETSI TS 102 221 11.1.1.4.9
class PS_DO(BER_TLV_IE, tag=0x90):
@@ -269,6 +287,33 @@ def tlv_val_interpret(inmap, indata):
return val
return {d[0]: newval(inmap, d[0], d[1]) for d in indata.items()}
+# TS 102 221 11.1.19.2.1
+class TerminalPowerSupply(BER_TLV_IE, tag=0x80):
+ _construct = Struct('used_supply_voltage_class'/SupplyVoltageClasses,
+ 'maximum_available_power_supply'/Int8ub,
+ 'actual_used_freq_100k'/Int8ub)
+
+# TS 102 221 11.1.19.2.2
+class ExtendedLchanTerminalSupport(BER_TLV_IE, tag=0x81):
+ _construct = GreedyBytes
+
+# TS 102 221 11.1.19.2.3
+class AdditionalInterfacesSupport(BER_TLV_IE, tag=0x82):
+ _construct = FlagsEnum(Int8ub, uicc_clf=0x01)
+
+# TS 102 221 11.1.19.2.4 + SGP.32 v3.0 3.4.2 RSP Device Capabilities
+class AdditionalTermCapEuicc(BER_TLV_IE, tag=0x83):
+ _construct = FlagsEnum(Int8ub, lui_d=0x01, lpd_d=0x02, lds_d=0x04, lui_e_scws=0x08,
+ metadata_update_alerting=0x10,
+ enterprise_capable_device=0x20,
+ lui_e_e4e=0x40,
+ lpr=0x80)
+
+# TS 102 221 11.1.19.2.0
+class TerminalCapability(BER_TLV_IE, tag=0xa9, nested=[TerminalPowerSupply, ExtendedLchanTerminalSupport,
+ AdditionalInterfacesSupport, AdditionalTermCapEuicc]):
+ pass
+
# ETSI TS 102 221 Section 9.2.7 + ISO7816-4 9.3.3/9.3.4
class _AM_DO_DF(DataObject):
def __init__(self):
@@ -466,12 +511,11 @@ class CRT_DO(DataObject):
def from_bytes(self, do: bytes):
"""Decode a Control Reference Template DO."""
if len(do) != 6:
- raise ValueError('Unsupported CRT DO length: %s', do)
+ raise ValueError('Unsupported CRT DO length: %s' %do)
if do[0] != 0x83 or do[1] != 0x01:
- raise ValueError('Unsupported Key Ref Tag or Len in CRT DO %s', do)
+ raise ValueError('Unsupported Key Ref Tag or Len in CRT DO %s' % do)
if do[3:] != b'\x95\x01\x08':
- raise ValueError(
- 'Unsupported Usage Qualifier Tag or Len in CRT DO %s', do)
+ raise ValueError('Unsupported Usage Qualifier Tag or Len in CRT DO %s' % do)
self.encoded = do[0:6]
self.decoded = pin_names[do[2]]
return do[6:]
@@ -505,7 +549,7 @@ class SecCondByte_DO(DataObject):
if inb & 0x10:
res.append('user_auth')
rd = {'mode': cond}
- if len(res):
+ if len(res) > 0:
rd['conditions'] = res
self.decoded = rd
@@ -581,6 +625,10 @@ class EF_DIR(LinFixedEF):
{ "application_label": "USim1" },
{ "discretionary_template": h2b("a00c80011781025f608203454150") } ] }
),
+ ( '61194f10a0000000871004ffffffff890709000050054953696d31',
+ { "application_template": [ { "application_id": h2b("a0000000871004ffffffff8907090000") },
+ { "application_label": "ISim1" } ] }
+ ),
]
class ApplicationLabel(BER_TLV_IE, tag=0x50):
# TODO: UCS-2 coding option as per Annex A of TS 102 221
@@ -620,6 +668,7 @@ class EF_PL(TransRecEF):
( '656e', "en" ),
( 'ffff', None ),
]
+
def __init__(self, fid='2f05', sfid=0x05, name='EF.PL', desc='Preferred Languages'):
super().__init__(fid, sfid=sfid, name=name,
desc=desc, rec_len=2, size=(2, None))
@@ -703,7 +752,7 @@ class EF_ARR(LinFixedEF):
raise ValueError
return by_mode
- def _decode_record_bin(self, raw_bin_data, **kwargs):
+ def _decode_record_bin(self, raw_bin_data, **_kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
dec = arr_seq.decode_multi(raw_bin_data)
@@ -711,20 +760,17 @@ class EF_ARR(LinFixedEF):
# 'un-flattening' decoder, and hence would be unable to encode :(
return dec[0]
- def _encode_record_bin(self, in_json, **kwargs):
+ def _encode_record_bin(self, in_json, **_kwargs):
# we can only guess if we should decode for EF or DF here :(
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
return arr_seq.encode_multi(in_json)
@with_default_category('File-Specific Commands')
class AddlShellCommands(CommandSet):
- def __init__(self):
- super().__init__()
-
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
def do_read_arr_record(self, opts):
"""Read one EF.ARR record in flattened, human-friendly form."""
- (data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
+ (data, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
data = self._cmd.lchan.selected_file.flatten(data)
self._cmd.poutput_json(data, opts.oneline)
@@ -735,7 +781,7 @@ class EF_ARR(LinFixedEF):
# collect all results in list so they are rendered as JSON list when printing
data_list = []
for recnr in range(1, 1 + num_of_rec):
- (data, sw) = self._cmd.lchan.read_record_dec(recnr)
+ (data, _sw) = self._cmd.lchan.read_record_dec(recnr)
data = self._cmd.lchan.selected_file.flatten(data)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
@@ -746,6 +792,8 @@ class EF_UMPC(TransparentEF):
_test_de_encode = [
( '3cff02', { "max_current_mA": 60, "t_op_s": 255,
"addl_info": { "req_inc_idle_current": False, "support_uicc_suspend": True } } ),
+ ( '320500', { "max_current_mA": 50, "t_op_s": 5, "addl_info": {"req_inc_idle_current": False,
+ "support_uicc_suspend": False } } ),
]
def __init__(self, fid='2f08', sfid=0x08, name='EF.UMPC', desc='UICC Maximum Power Consumption'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=(5, 5))
@@ -768,6 +816,11 @@ class CardProfileUICC(CardProfile):
# FIXME: DF.CD
EF_UMPC(),
]
+ addons = [
+ AddonSIM,
+ AddonGSMR,
+ AddonRUIM,
+ ]
sw = {
'Normal': {
'9000': 'Normal ending of the command',
@@ -781,7 +834,7 @@ class CardProfileUICC(CardProfile):
'6200': 'No information given, state of non-volatile memory unchanged',
'6281': 'Part of returned data may be corrupted',
'6282': 'End of file/record reached before reading Le bytes or unsuccessful search',
- '6283': 'Selected file invalidated',
+ '6283': 'Selected file invalidated/disabled; needs to be activated before use',
'6284': 'Selected file in termination state',
'62f1': 'More data available',
'62f2': 'More data available and proactive command pending',
@@ -839,13 +892,13 @@ class CardProfileUICC(CardProfile):
super().__init__(name, desc='ETSI TS 102 221', cla="00",
sel_ctrl="0004", files_in_mf=files, sw=sw,
- shell_cmdsets = [self.AddlShellCommands()])
+ shell_cmdsets = [self.AddlShellCommands()], addons = addons)
@staticmethod
- def decode_select_response(resp_hex: str) -> object:
+ def decode_select_response(data_hex: str) -> object:
"""ETSI TS 102 221 Section 11.1.1.3"""
t = FcpTemplate()
- t.from_tlv(h2b(resp_hex))
+ t.from_tlv(h2b(data_hex))
d = t.to_dict()
return flatten_dict_lists(d['fcp_template'])
@@ -880,19 +933,80 @@ class CardProfileUICC(CardProfile):
commands are permitted between SUSPEND and RESUME. See TS 102 221 Section 11.1.22."""
self._cmd.card._scc.resume_uicc(opts.token)
-
-class CardProfileUICCSIM(CardProfileUICC):
- """Same as above, but including 2G SIM support"""
-
- ORDER = 0
-
- def __init__(self):
- super().__init__('UICC-SIM')
-
- # Add GSM specific files
- self.files_in_mf.append(DF_TELECOM())
- self.files_in_mf.append(DF_GSM())
-
- @staticmethod
- def match_with_card(scc: SimCardCommands) -> bool:
- return match_uicc(scc) and match_sim(scc)
+ term_cap_parser = argparse.ArgumentParser()
+ # power group
+ tc_power_grp = term_cap_parser.add_argument_group('Terminal Power Supply')
+ tc_power_grp.add_argument('--used-supply-voltage-class', type=str, choices=['a','b','c','d','e'],
+ help='Actual used Supply voltage class')
+ tc_power_grp.add_argument('--maximum-available-power-supply', type=auto_uint8,
+ help='Maximum available power supply of the terminal')
+ tc_power_grp.add_argument('--actual-used-freq-100k', type=auto_uint8,
+ help='Actual used clock frequency (in units of 100kHz)')
+ # no separate groups for those two
+ tc_elc_grp = term_cap_parser.add_argument_group('Extended logical channels terminal support')
+ tc_elc_grp.add_argument('--extended-logical-channel', action='store_true',
+ help='Extended Logical Channel supported')
+ tc_aif_grp = term_cap_parser.add_argument_group('Additional interfaces support')
+ tc_aif_grp.add_argument('--uicc-clf', action='store_true',
+ help='Local User Interface in the Device (LUId) supported')
+ # eUICC group
+ tc_euicc_grp = term_cap_parser.add_argument_group('Additional Terminal capability indications related to eUICC')
+ tc_euicc_grp.add_argument('--lui-d', action='store_true',
+ help='Local User Interface in the Device (LUId) supported')
+ tc_euicc_grp.add_argument('--lpd-d', action='store_true',
+ help='Local Profile Download in the Device (LPDd) supported')
+ tc_euicc_grp.add_argument('--lds-d', action='store_true',
+ help='Local Discovery Service in the Device (LPDd) supported')
+ tc_euicc_grp.add_argument('--lui-e-scws', action='store_true',
+ help='LUIe based on SCWS supported')
+ tc_euicc_grp.add_argument('--metadata-update-alerting', action='store_true',
+ help='Metadata update alerting supported')
+ tc_euicc_grp.add_argument('--enterprise-capable-device', action='store_true',
+ help='Enterprise Capable Device')
+ tc_euicc_grp.add_argument('--lui-e-e4e', action='store_true',
+ help='LUIe using E4E (ENVELOPE tag E4) supported')
+ tc_euicc_grp.add_argument('--lpr', action='store_true',
+ help='LPR (LPA Proxy) supported')
+
+ @cmd2.with_argparser(term_cap_parser)
+ def do_terminal_capability(self, opts):
+ """Perform the TERMINAL CAPABILITY function. Used to inform the UICC about terminal capability."""
+ ps_flags = {}
+ addl_if_flags = {}
+ euicc_flags = {}
+
+ opts_dict = vars(opts)
+
+ power_items = ['used_supply_voltage_class', 'maximum_available_power_supply', 'actual_used_freq_100k']
+ if any(opts_dict[x] for x in power_items):
+ if not all(opts_dict[x] for x in power_items):
+ raise argparse.ArgumentTypeError('If any of the Terminal Power Supply group options are used, all must be specified')
+
+ for k, v in opts_dict.items():
+ if k in AdditionalInterfacesSupport._construct.flags.keys():
+ addl_if_flags[k] = v
+ elif k in AdditionalTermCapEuicc._construct.flags.keys():
+ euicc_flags[k] = v
+ elif k in [f.name for f in TerminalPowerSupply._construct.subcons]:
+ if k == 'used_supply_voltage_class' and v:
+ v = {v: True}
+ ps_flags[k] = v
+
+ child_list = []
+ if any(x for x in ps_flags.values()):
+ child_list.append(TerminalPowerSupply(decoded=ps_flags))
+
+ if opts.extended_logical_channel:
+ child_list.append(ExtendedLchanTerminalSupport())
+ if any(x for x in addl_if_flags.values()):
+ child_list.append(AdditionalInterfacesSupport(decoded=addl_if_flags))
+ if any(x for x in euicc_flags.values()):
+ child_list.append(AdditionalTermCapEuicc(decoded=euicc_flags))
+
+ print(child_list)
+ tc = TerminalCapability(children=child_list)
+ self.terminal_capability(b2h(tc.to_tlv()))
+
+ def terminal_capability(self, data:Hexstr):
+ cmd_hex = "80AA0000%02x%s" % (len(data)//2, data)
+ _rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
diff --git a/pySim/ts_102_222.py b/pySim/ts_102_222.py
index f8fdddb..4834ce8 100644
--- a/pySim/ts_102_222.py
+++ b/pySim/ts_102_222.py
@@ -18,16 +18,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List
-
-import cmd2
-from cmd2 import CommandSet, with_default_category, with_argparser
import argparse
-from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
-from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
+import cmd2
+from cmd2 import CommandSet, with_default_category
-from pySim.exceptions import *
-from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder
+from pySim.utils import b2h, auto_uint8, auto_uint16, is_hexstr
from pySim.ts_102_221 import *
@@ -35,9 +31,6 @@ from pySim.ts_102_221 import *
class Ts102222Commands(CommandSet):
"""Administrative commands for telecommunication applications."""
- def __init__(self):
- super().__init__()
-
delfile_parser = argparse.ArgumentParser()
delfile_parser.add_argument('--force-delete', action='store_true',
help='I really want to permanently delete the file. I know pySim cannot re-create it yet!')
@@ -52,7 +45,7 @@ class Ts102222Commands(CommandSet):
self._cmd.perror("Refusing to permanently delete the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
- (data, sw) = self._cmd.card._scc.delete_file(f.fid)
+ (_data, _sw) = self._cmd.lchan.scc.delete_file(f.fid)
def complete_delete_file(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for DELETE FILE"""
@@ -73,7 +66,7 @@ class Ts102222Commands(CommandSet):
self._cmd.perror("Refusing to terminate the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
- (data, sw) = self._cmd.card._scc.terminate_df(f.fid)
+ (_data, _sw) = self._cmd.lchan.scc.terminate_df(f.fid)
def complete_terminate_df(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for TERMINATE DF"""
@@ -89,7 +82,7 @@ class Ts102222Commands(CommandSet):
self._cmd.perror("Refusing to terminate the file, please read the help text.")
return
f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
- (data, sw) = self._cmd.card._scc.terminate_ef(f.fid)
+ (_data, _sw) = self._cmd.lchan.scc.terminate_ef(f.fid)
def complete_terminate_ef(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for TERMINATE EF"""
@@ -107,21 +100,21 @@ class Ts102222Commands(CommandSet):
if not opts.force_terminate_card:
self._cmd.perror("Refusing to permanently terminate the card, please read the help text.")
return
- (data, sw) = self._cmd.card._scc.terminate_card_usage()
+ (_data, _sw) = self._cmd.lchan.scc.terminate_card_usage()
create_parser = argparse.ArgumentParser()
- create_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string')
+ create_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
create_parser._action_groups.pop()
create_required = create_parser.add_argument_group('required arguments')
create_optional = create_parser.add_argument_group('optional arguments')
create_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
- create_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR')
- create_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets')
+ create_required.add_argument('--ef-arr-record-nr', required=True, type=auto_uint8, help='Referenced Security: Record Number within EF.ARR')
+ create_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
create_required.add_argument('--structure', required=True, type=str, choices=['transparent', 'linear_fixed', 'ber_tlv'],
help='Structure of the to-be-created EF')
create_optional.add_argument('--short-file-id', type=str, help='Short File Identifier as 2-digit hex string')
create_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?')
- create_optional.add_argument('--record-length', type=int, help='Length of each record in octets')
+ create_optional.add_argument('--record-length', type=auto_uint16, help='Length of each record in octets')
@cmd2.with_argparser(create_parser)
def do_create_ef(self, opts):
@@ -152,22 +145,22 @@ class Ts102222Commands(CommandSet):
ShortFileIdentifier(decoded=opts.short_file_id),
]
fcp = FcpTemplate(children=ies)
- (data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv()))
+ (_data, _sw) = self._cmd.lchan.scc.create_file(b2h(fcp.to_tlv()))
# the newly-created file is automatically selected but our runtime state knows nothing of it
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
createdf_parser = argparse.ArgumentParser()
- createdf_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string')
+ createdf_parser.add_argument('FILE_ID', type=is_hexstr, help='File Identifier as 4-character hex string')
createdf_parser._action_groups.pop()
createdf_required = createdf_parser.add_argument_group('required arguments')
createdf_optional = createdf_parser.add_argument_group('optional arguments')
createdf_sja_optional = createdf_parser.add_argument_group('sysmoISIM-SJA optional arguments')
createdf_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
- createdf_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR')
+ createdf_required.add_argument('--ef-arr-record-nr', required=True, type=auto_uint8, help='Referenced Security: Record Number within EF.ARR')
createdf_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?')
- createdf_optional.add_argument('--aid', type=str, help='Application ID (creates an ADF, instead of a DF)')
+ createdf_optional.add_argument('--aid', type=is_hexstr, help='Application ID (creates an ADF, instead of a DF)')
# mandatory by spec, but ignored by several OS, so don't force the user
- createdf_optional.add_argument('--total-file-size', type=int, help='Physical memory allocated for DF/ADi in octets')
+ createdf_optional.add_argument('--total-file-size', type=auto_uint16, help='Physical memory allocated for DF/ADi in octets')
createdf_sja_optional.add_argument('--permit-rfm-create', action='store_true')
createdf_sja_optional.add_argument('--permit-rfm-delete-terminate', action='store_true')
createdf_sja_optional.add_argument('--permit-other-applet-create', action='store_true')
@@ -195,15 +188,15 @@ class Ts102222Commands(CommandSet):
ies.append(TotalFileSize(decoded=opts.total_file_size))
# TODO: Spec states PIN Status Template DO is mandatory
if opts.permit_rfm_create or opts.permit_rfm_delete_terminate or opts.permit_other_applet_create or opts.permit_other_applet_delete_terminate:
- toolkit_ac = {
- 'rfm_create': opts.permit_rfm_create,
- 'rfm_delete_terminate': opts.permit_rfm_delete_terminate,
- 'other_applet_create': opts.permit_other_applet_create,
- 'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate,
- }
- ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)]))
+ toolkit_ac = {
+ 'rfm_create': opts.permit_rfm_create,
+ 'rfm_delete_terminate': opts.permit_rfm_delete_terminate,
+ 'other_applet_create': opts.permit_other_applet_create,
+ 'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate,
+ }
+ ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)]))
fcp = FcpTemplate(children=ies)
- (data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv()))
+ (_data, _sw) = self._cmd.lchan.scc.create_file(b2h(fcp.to_tlv()))
# the newly-created file is automatically selected but our runtime state knows nothing of it
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
@@ -211,7 +204,7 @@ class Ts102222Commands(CommandSet):
resize_ef_parser.add_argument('NAME', type=str, help='Name or FID of file to be resized')
resize_ef_parser._action_groups.pop()
resize_ef_required = resize_ef_parser.add_argument_group('required arguments')
- resize_ef_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets')
+ resize_ef_required.add_argument('--file-size', required=True, type=auto_uint16, help='Size of file in octets')
@cmd2.with_argparser(resize_ef_parser)
def do_resize_ef(self, opts):
@@ -220,7 +213,7 @@ class Ts102222Commands(CommandSet):
ies = [FileIdentifier(decoded=f.fid),
FileSize(decoded=opts.file_size)]
fcp = FcpTemplate(children=ies)
- (data, sw) = self._cmd.card._scc.resize_file(b2h(fcp.to_tlv()))
+ (_data, _sw) = self._cmd.lchan.scc.resize_file(b2h(fcp.to_tlv()))
# the resized file is automatically selected but our runtime state knows nothing of it
self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
diff --git a/pySim/ts_102_310.py b/pySim/ts_102_310.py
new file mode 100644
index 0000000..ea3a448
--- /dev/null
+++ b/pySim/ts_102_310.py
@@ -0,0 +1,114 @@
+# coding=utf-8
+"""Utilities / Functions related to ETSI TS 102 310, the EAP UICC spec.
+
+(C) 2024 by Harald Welte <laforge@osmocom.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from pySim.construct import *
+from construct import *
+from construct import Optional as COptional
+
+#from pySim.utils import *
+from pySim.filesystem import CardDF, TransparentEF
+from pySim.tlv import BER_TLV_IE, TLV_IE_Collection
+
+# TS102 310 Section 7.1
+class EF_EAPKEYS(TransparentEF):
+ class Msk(BER_TLV_IE, tag=0x80):
+ _construct = HexAdapter(GreedyBytes)
+ class Emsk(BER_TLV_IE, tag=0x81):
+ _construct = HexAdapter(GreedyBytes)
+ class MskCollection(TLV_IE_Collection, nested=[EF_EAPKEYS.Msk, EF_EAPKEYS.Emsk]):
+ pass
+
+ def __init__(self, fid='4f01', name='EF.EAPKEYS', desc='EAP derived keys'):
+ super().__init__(fid, sfid=0x01, name=name, desc=desc, size=(1,None))
+ self._tlv = EF_EAPKEYS.MskCollection
+
+# TS 102 310 Section 7.2
+class EF_EAPSTATUS(TransparentEF):
+ def __init__(self, fid='4f02', name='EF.EAPSTATUS', desc='EAP Authentication Status'):
+ super().__init__(fid, sfid=0x02, name=name, desc=desc, size=(1,1))
+ self._construct = Enum(Int8ub, no_auth_started=0, authenticating=1,
+ authenticated=2, held_auth_failure=3)
+
+# TS 102 310 Section 7.3
+class EF_PUId(TransparentEF):
+ def __init__(self, fid='4f03', name='EF.PUId', desc='Permanent User Identity'):
+ super().__init__(fid, sfid=0x03, name=name, desc=desc, size=(10,None))
+ self._construct = GreedyBytes
+
+# TS 102 310 Section 7.4
+class EF_Ps(TransparentEF):
+ def __init__(self, fid='4f04', name='EF.Ps', desc='Pseudonym'):
+ super().__init__(fid, sfid=0x04, name=name, desc=desc, size=(1,None))
+ self._construct = GreedyBytes
+
+# TS 102 310 Section 7.5
+class EF_CurID(TransparentEF):
+ def __init__(self, fid='4f20', name='EF.CurID', desc='Current Identity'):
+ super().__init__(fid, sfid=0x10, name=name, desc=desc, size=(1,None))
+ self._construct = Struct('type'/Enum(Int8ub, permanent=0, pseudonym=1, re_authentication=2, should_not_be_revealed=255),
+ '_len'/Int8ub,
+ 'value'/Utf8Adapter(this._len))
+
+
+# TS 102 310 Section 7.6
+class EF_ReID(TransparentEF):
+ class Identity(BER_TLV_IE, tag=0x80):
+ _construct = Utf8Adapter(GreedyBytes)
+ class Counter(BER_TLV_IE, tag=0x81):
+ _construct = GreedyInteger
+ class Collection(TLV_IE_Collection, nested=[EF_ReID.Identity, EF_ReID.Counter]):
+ pass
+
+ def __init__(self, fid='4f21', name='EF.ReID', desc='Re-Authentication Identity'):
+ super().__init__(fid, sfid=0x11, name=name, desc=desc, size=(1,None))
+ self._tlv = EF_ReID.Collection
+
+# TS 102 310 Section 7.7
+class EF_Realm(TransparentEF):
+ def __init__(self, fid='4f22', name='EF.Realm', desc='Relm value of the identity'):
+ super().__init__(fid, sfid=0x12, name=name, desc=desc, size=(1,None))
+ self._construct = Struct('_len'/Int8ub,
+ 'realm'/Utf8Adapter(Bytes(this._len)))
+
+class DF_EAP(CardDF):
+ # DF.EAP has no default FID; it always must be discovered via the EF.DIR entry
+ # and the 0x73 "discretionary template"
+ def __init__(self, fid, name='DF.EAP', desc='EAP client', **kwargs):
+ super().__init__(fid=fid, name=name, desc=desc, **kwargs)
+ files = [
+ EF_EAPKEYS(),
+ EF_EAPSTATUS(),
+ EF_PUId(),
+ EF_CurID(),
+ EF_ReID(),
+ ]
+ self.add_files(files)
+
+
+# TS 102 310 Section 5.2
+class EapSupportedTypesList(BER_TLV_IE, tag=0x80):
+ _construct = GreedyRange(Int8ub)
+class EapDedicatedFilesList(BER_TLV_IE, tag=0x81):
+ _construct = GreedyRange(Int16ub)
+class EapLabel(BER_TLV_IE, tag=0x82):
+ _construct = GreedyBytes
+class EapAppSvcSpecData(BER_TLV_IE, tag=0xa0, nested=[EapSupportedTypesList, EapDedicatedFilesList, EapLabel]):
+ pass
+class DiscretionaryTemplate(BER_TLV_IE, tag=0x73, nested=[EapAppSvcSpecData]):
+ pass
diff --git a/pySim/ts_31_102.py b/pySim/ts_31_102.py
index e7ae8f0..2536603 100644
--- a/pySim/ts_31_102.py
+++ b/pySim/ts_31_102.py
@@ -10,7 +10,7 @@ Various constants from 3GPP TS 31.102 V17.9.0
#
# Copyright (C) 2020 Supreeth Herle <herlesupreeth@gmail.com>
-# Copyright (C) 2021-2023 Harald Welte <laforge@osmocom.org>
+# Copyright (C) 2021-2024 Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -26,12 +26,17 @@ Various constants from 3GPP TS 31.102 V17.9.0
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-# Mapping between USIM Service Number and its description
+import enum
+
+from construct import Optional as COptional
+from construct import Int32ub, Nibble, GreedyRange, Struct, FlagsEnum, Switch, this, Int16ub, Padding
+from construct import Bytewise, Int24ub, PaddedString
+
import pySim.ts_102_221
from pySim.ts_51_011 import EF_ACMmax, EF_AAeM, EF_eMLPP, EF_CMI, EF_PNN
from pySim.ts_51_011 import EF_MMSN, EF_MMSICP, EF_MMSUP, EF_MMSUCP, EF_VGCS, EF_VGCSS, EF_NIA
from pySim.ts_51_011 import EF_SMSR, EF_DCK, EF_EXT, EF_CNL, EF_OPL, EF_MBI, EF_MWIS
-from pySim.ts_51_011 import EF_CBMID, EF_CBMIR, EF_ADN, EF_SMS, EF_MSISDN, EF_SMSP, EF_SMSS
+from pySim.ts_51_011 import EF_CBMID, EF_CBMIR, EF_ADN, EF_CFIS, EF_SMS, EF_MSISDN, EF_SMSP, EF_SMSS
from pySim.ts_51_011 import EF_IMSI, EF_xPLMNwAcT, EF_SPN, EF_CBMI, EF_ACC, EF_PLMNsel
from pySim.ts_51_011 import EF_Kc, EF_CPBCCH, EF_InvScan
from pySim.ts_102_221 import EF_ARR
@@ -39,12 +44,10 @@ from pySim.tlv import *
from pySim.filesystem import *
from pySim.ts_31_102_telecom import DF_PHONEBOOK, EF_UServiceTable
from pySim.construct import *
+from pySim.utils import is_hexstr
from pySim.cat import SMS_TPDU, DeviceIdentities, SMSPPDownload
-from construct import Optional as COptional
-from construct import *
-from typing import Tuple
-from struct import unpack, pack
-import enum
+
+# Mapping between USIM Service Number and its description
EF_UST_map = {
1: 'Local Phone Book',
2: 'Fixed Dialling Numbers (FDN)',
@@ -209,117 +212,6 @@ EF_EST_map = {
3: 'APN Control List (ACL)'
}
-LOCI_STATUS_map = {
- 0: 'updated',
- 1: 'not updated',
- 2: 'plmn not allowed',
- 3: 'locatation area not allowed'
-}
-
-EF_USIM_ADF_map = {
- 'LI': '6F05',
- 'ARR': '6F06',
- 'IMSI': '6F07',
- 'Keys': '6F08',
- 'KeysPS': '6F09',
- 'DCK': '6F2C',
- 'HPPLMN': '6F31',
- 'CNL': '6F32',
- 'ACMmax': '6F37',
- 'UST': '6F38',
- 'ACM': '6F39',
- 'FDN': '6F3B',
- 'SMS': '6F3C',
- 'GID1': '6F3E',
- 'GID2': '6F3F',
- 'MSISDN': '6F40',
- 'PUCT': '6F41',
- 'SMSP': '6F42',
- 'SMSS': '6F42',
- 'CBMI': '6F45',
- 'SPN': '6F46',
- 'SMSR': '6F47',
- 'CBMID': '6F48',
- 'SDN': '6F49',
- 'EXT2': '6F4B',
- 'EXT3': '6F4C',
- 'BDN': '6F4D',
- 'EXT5': '6F4E',
- 'CCP2': '6F4F',
- 'CBMIR': '6F50',
- 'EXT4': '6F55',
- 'EST': '6F56',
- 'ACL': '6F57',
- 'CMI': '6F58',
- 'START-HFN': '6F5B',
- 'THRESHOLD': '6F5C',
- 'PLMNwAcT': '6F60',
- 'OPLMNwAcT': '6F61',
- 'HPLMNwAcT': '6F62',
- 'PSLOCI': '6F73',
- 'ACC': '6F78',
- 'FPLMN': '6F7B',
- 'LOCI': '6F7E',
- 'ICI': '6F80',
- 'OCI': '6F81',
- 'ICT': '6F82',
- 'OCT': '6F83',
- 'AD': '6FAD',
- 'VGCS': '6FB1',
- 'VGCSS': '6FB2',
- 'VBS': '6FB3',
- 'VBSS': '6FB4',
- 'eMLPP': '6FB5',
- 'AAeM': '6FB6',
- 'ECC': '6FB7',
- 'Hiddenkey': '6FC3',
- 'NETPAR': '6FC4',
- 'PNN': '6FC5',
- 'OPL': '6FC6',
- 'MBDN': '6FC7',
- 'EXT6': '6FC8',
- 'MBI': '6FC9',
- 'MWIS': '6FCA',
- 'CFIS': '6FCB',
- 'EXT7': '6FCC',
- 'SPDI': '6FCD',
- 'MMSN': '6FCE',
- 'EXT8': '6FCF',
- 'MMSICP': '6FD0',
- 'MMSUP': '6FD1',
- 'MMSUCP': '6FD2',
- 'NIA': '6FD3',
- 'VGCSCA': '6FD4',
- 'VBSCA': '6FD5',
- 'GBAP': '6FD6',
- 'MSK': '6FD7',
- 'MUK': '6FD8',
- 'EHPLMN': '6FD9',
- 'GBANL': '6FDA',
- 'EHPLMNPI': '6FDB',
- 'LRPLMNSI': '6FDC',
- 'NAFKCA': '6FDD',
- 'SPNI': '6FDE',
- 'PNNI': '6FDF',
- 'NCP-IP': '6FE2',
- 'EPSLOCI': '6FE3',
- 'EPSNSC': '6FE4',
- 'UFC': '6FE6',
- 'UICCIARI': '6FE7',
- 'NASCONFIG': '6FE8',
- 'PWC': '6FEC',
- 'FDNURI': '6FED',
- 'BDNURI': '6FEE',
- 'SDNURI': '6FEF',
- 'IWL': '6FF0',
- 'IPS': '6FF1',
- 'IPD': '6FF2',
- 'ePDGId': '6FF3',
- 'ePDGSelection': '6FF4',
- 'ePDGIdEm': '6FF5',
- 'ePDGSelectionEm': '6FF6',
-}
-
# 3gPP TS 31.102 Section 7.5.2.1
class SUCI_TlvDataObject(BER_TLV_IE, tag=0xA1):
_construct = HexAdapter(GreedyBytes)
@@ -378,35 +270,40 @@ class EF_5GAUTHKEYS(TransparentEF):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._tlv = EF_5GAUTHKEYS.FiveGAuthKeys
-# 3GPP TS 31.102 Section 4.4.11.8
-class ProtSchemeIdList(BER_TLV_IE, tag=0xa0):
- # FIXME: 3GPP TS 24.501 Protection Scheme Identifier
- # repeated sequence of (id, index) tuples
- _construct = GreedyRange(
- Struct('id'/Enum(Byte, null=0, A=1, B=2), 'index'/Int8ub))
-
-
-class HomeNetPubKeyId(BER_TLV_IE, tag=0x80):
- # 3GPP TS 24.501 / 3GPP TS 23.003
- _construct = Int8ub
-
-
-class HomeNetPubKey(BER_TLV_IE, tag=0x81):
- # FIXME: RFC 5480
- _construct = HexAdapter(GreedyBytes)
-
+# TS 31.102 4.4.11.8
+class EF_SUCI_Calc_Info(TransparentEF):
+ _test_de_encode = [
+ ( 'A00401010000A14A80010A81204E858C4D49D1343E6181284C47CA721730C98742CB7C6182D2E8126E08088D3680010B8120D1BC365F4997D17CE4374E72181431CBFEBA9E1B98D7618F79D48561B144672A',
+ {"prot_scheme_id_list": [{"priority": 0, "identifier": 1, "key_index": 1}, {"priority": 1,
+ "identifier": 0,
+ "key_index": 0}],
+ "hnet_pubkey_list": [{"hnet_pubkey_identifier": 10, "hnet_pubkey":
+ "4e858c4d49d1343e6181284c47ca721730c98742cb7c6182d2e8126e08088d36"},
+ {"hnet_pubkey_identifier": 11, "hnet_pubkey":
+ "d1bc365f4997d17ce4374e72181431cbfeba9e1b98d7618f79d48561b144672a"}]} ),
+ ]
+ # 3GPP TS 31.102 Section 4.4.11.8
+ class ProtSchemeIdList(BER_TLV_IE, tag=0xa0):
+ # FIXME: 3GPP TS 24.501 Protection Scheme Identifier
+ # repeated sequence of (id, index) tuples
+ _construct = GreedyRange(
+ Struct('identifier'/Enum(Byte, null=0, A=1, B=2), 'key_index'/Int8ub))
+
+ class HnetPubkeyIdentifier(BER_TLV_IE, tag=0x80):
+ # 3GPP TS 24.501 / 3GPP TS 23.003
+ _construct = Int8ub
-class HomeNetPubKeyList(BER_TLV_IE, tag=0xa1,
- nested=[HomeNetPubKeyId, HomeNetPubKey]):
- pass
+ class HnetPubkey(BER_TLV_IE, tag=0x81):
+ # contents according to RFC 7748 / RFC 5480
+ _construct = HexAdapter(GreedyBytes)
-# 3GPP TS 31.102 Section 4.4.11.6
-class SUCI_CalcInfo(TLV_IE_Collection, nested=[ProtSchemeIdList, HomeNetPubKeyList]):
- pass
+ class HnetPubkeyList(BER_TLV_IE, tag=0xa1, nested=[HnetPubkeyIdentifier, HnetPubkey]):
+ pass
+ # 3GPP TS 31.102 Section 4.4.11.6
+ class SUCI_CalcInfo(TLV_IE_Collection, nested=[ProtSchemeIdList, HnetPubkeyList]):
+ pass
-# TS 31.102 4.4.11.8
-class EF_SUCI_Calc_Info(TransparentEF):
def __init__(self, fid="4f07", sfid=0x07, name='EF.SUCI_Calc_Info', size=(2, None),
desc='SUCI Calc Info', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
@@ -422,30 +319,35 @@ class EF_SUCI_Calc_Info(TransparentEF):
return out_bytes
- def _encode_hnet_pubkey_list(self, hnet_pubkey_list):
- out_bytes = [0xa1] # pubkey list tag
- out_bytes.append(0x00) # length filled later
- length = 0
-
- for key in hnet_pubkey_list:
- out_bytes.append(0x80) # identifier tag
- out_bytes.append(0x01) # TODO size, fixed to 1 byte
- out_bytes.append(key["hnet_pubkey_identifier"])
- out_bytes.append(0x81) # key tag
- out_bytes.append(len(key["hnet_pubkey"])//2)
- length += 5+len(key["hnet_pubkey"])//2
-
- pubkey_bytes = h2b(key["hnet_pubkey"])
- out_bytes += pubkey_bytes
-
- # fill length
- out_bytes[1] = length
- return out_bytes
+ @staticmethod
+ def _compact_pubkey_list(l: List[dict]) -> List[dict]:
+ """conversion method to generate list of {hnet_pubkey_identifier, hnet_pubkey} dicts
+ from flat [{hnet_pubkey_identifier: }, {net_pubkey: }, ...] list"""
+ out = []
+ while len(l):
+ a = l.pop(0)
+ b = l.pop(0)
+ z = {**a, **b}
+ out.append(z)
+ return out
+
+ @staticmethod
+ def _expand_pubkey_list(l: List[dict]) -> List[dict]:
+ """conversion method to generate flat [{hnet_pubkey_identifier: }, {net_pubkey: }, ...] list
+ from compacted list of {hnet_pubkey_identifier, hnet_pubkey} dicts"""
+ out = []
+ for d in l:
+ for k, v in d.items():
+ out.append({k: v})
+ return out
def _encode_hex(self, in_json):
out_bytes = self._encode_prot_scheme_id_list(
in_json['prot_scheme_id_list'])
- out_bytes += self._encode_hnet_pubkey_list(in_json['hnet_pubkey_list'])
+ d = self._expand_pubkey_list(in_json['hnet_pubkey_list'])
+ hpkl = EF_SUCI_Calc_Info.HnetPubkeyList()
+ hpkl.from_dict({'hnet_pubkey_list': d})
+ out_bytes += hpkl.to_tlv()
return "".join(["%02X" % i for i in out_bytes])
def _decode_prot_scheme_id_list(self, in_bytes):
@@ -462,42 +364,6 @@ class EF_SUCI_Calc_Info(TransparentEF):
prot_scheme_id_list.append(prot_scheme)
return prot_scheme_id_list
- def _decode_hnet_pubkey_list(self, in_bytes):
- hnet_pubkey_list = []
- pos = 0
- if in_bytes[pos] != 0xa1:
- print("missing Home Network Public Key List data object")
- return {}
- pos += 1
- hnet_pubkey_list_len = in_bytes[pos]
- pos += 1
-
- while pos < hnet_pubkey_list_len:
- if in_bytes[pos] != 0x80:
- print("missing Home Network Public Key Identifier tag")
- return {}
- pos += 1
- # TODO might be more than 1 byte?
- hnet_pubkey_id_len = in_bytes[pos]
- pos += 1
- hnet_pubkey_id = in_bytes[pos:pos+hnet_pubkey_id_len][0]
- pos += hnet_pubkey_id_len
- if in_bytes[pos] != 0x81:
- print("missing Home Network Public Key tag")
- return {}
- pos += 1
- hnet_pubkey_len = in_bytes[pos]
- pos += 1
- hnet_pubkey = in_bytes[pos:pos+hnet_pubkey_len]
- pos += hnet_pubkey_len
-
- hnet_pubkey_list.append({
- 'hnet_pubkey_identifier': hnet_pubkey_id,
- 'hnet_pubkey': b2h(hnet_pubkey)
- })
-
- return hnet_pubkey_list
-
def _decode_bin(self, in_bin):
return self._decode_hex(b2h(in_bin))
@@ -518,7 +384,9 @@ class EF_SUCI_Calc_Info(TransparentEF):
pos += prot_scheme_id_list_len
# remaining data holds Home Network Public Key Data Object
- hnet_pubkey_list = self._decode_hnet_pubkey_list(in_bytes[pos:])
+ hpkl = EF_SUCI_Calc_Info.HnetPubkeyList()
+ hpkl.from_tlv(in_bytes[pos:])
+ hnet_pubkey_list = self._compact_pubkey_list(hpkl.to_dict()['hnet_pubkey_list'])
return {
'prot_scheme_id_list': prot_scheme_id_list,
@@ -534,15 +402,15 @@ class EF_LI(TransRecEF):
desc='Language Indication'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len)
- def _decode_record_bin(self, in_bin, **kwargs):
+ def _decode_record_bin(self, in_bin, **_kwargs):
if in_bin == b'\xff\xff':
return None
else:
# officially this is 7-bit GSM alphabet with one padding bit in each byte
return in_bin.decode('ascii')
- def _encode_record_bin(self, in_json, **kwargs):
- if in_json == None:
+ def _encode_record_bin(self, in_json, **_kwargs):
+ if in_json is None:
return b'\xff\xff'
else:
# officially this is 7-bit GSM alphabet with one padding bit in each byte
@@ -572,18 +440,17 @@ class EF_UST(EF_UServiceTable):
@with_default_category('File-Specific Commands')
class AddlShellCommands(CommandSet):
- def __init__(self):
- super().__init__()
-
def do_ust_service_activate(self, arg):
"""Activate a service within EF.UST"""
- self._cmd.card.update_ust(int(arg), 1)
+ selected_file = self._cmd.lchan.selected_file
+ selected_file.ust_update(self._cmd, [int(arg)], [])
def do_ust_service_deactivate(self, arg):
"""Deactivate a service within EF.UST"""
- self._cmd.card.update_ust(int(arg), 0)
+ selected_file = self._cmd.lchan.selected_file
+ selected_file.ust_update(self._cmd, [], [int(arg)])
- def do_ust_service_check(self, arg):
+ def do_ust_service_check(self, _arg):
"""Check consistency between services of this file and files present/activated.
Many services determine if one or multiple files shall be present/activated or if they shall be
@@ -625,16 +492,17 @@ class EF_ECC(LinFixedEF):
"marine_guard": False, "mountain_rescue": False,
"manual_ecall": False, "automatic_ecall": False } } ),
]
+ _test_no_pad = True
cc_construct = BcdAdapter(Rpad(Bytes(3)))
category_construct = FlagsEnum(Byte, police=1, ambulance=2, fire_brigade=3, marine_guard=4,
mountain_rescue=5, manual_ecall=6, automatic_ecall=7)
- alpha_construct = GsmStringAdapter(Rpad(GreedyBytes))
+ alpha_construct = GsmOrUcs2Adapter(Rpad(GreedyBytes))
def __init__(self, fid='6fb7', sfid=0x01, name='EF.ECC',
desc='Emergency Call Codes'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(4, 20))
- def _decode_record_bin(self, in_bin, **kwargs):
+ def _decode_record_bin(self, in_bin, **_kwargs):
# mandatory parts
code = in_bin[:3]
if code == b'\xff\xff\xff':
@@ -648,7 +516,7 @@ class EF_ECC(LinFixedEF):
ret['alpha_id'] = parse_construct(EF_ECC.alpha_construct, alpha_id)
return ret
- def _encode_record_bin(self, in_json, **kwargs):
+ def _encode_record_bin(self, in_json, **_kwargs):
if in_json is None:
return b'\xff\xff\xff\xff'
code = EF_ECC.cc_construct.build(in_json['call_code'])
@@ -665,12 +533,14 @@ class EF_ECC(LinFixedEF):
class EF_LOCI(TransparentEF):
_test_de_encode = [
( '47d1264a62f21037211e00',
- { "tmsi": "47d1264a", "lai": { "mcc_mnc": "262f01", "lac": "3721" },
+ { "tmsi": "47d1264a", "lai": { "mcc_mnc": "262-01", "lac": "3721" },
"rfu": 30, "lu_status": 0 } ),
+ ( 'ffffffff62f2200000ff01',
+ {"tmsi": "ffffffff", "lai": {"mcc_mnc": "262-02", "lac": "0000"}, "rfu": 255, "lu_status": 1} ),
]
def __init__(self, fid='6f7e', sfid=0x0b, name='EF.LOCI', desc='Location information', size=(11, 11)):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
- Lai = Struct('mcc_mnc'/BcdAdapter(Bytes(3)), 'lac'/HexAdapter(Bytes(2)))
+ Lai = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)), 'lac'/HexAdapter(Bytes(2)))
self._construct = Struct('tmsi'/HexAdapter(Bytes(4)), 'lai'/Lai, 'rfu'/Int8ub, 'lu_status'/Int8ub)
# TS 31.102 Section 4.2.18
@@ -685,6 +555,8 @@ class EF_AD(TransparentEF):
"prose_services": False, "extended_drx": False },
"rfu": 0, "mnc_len": 2, "extensions": b'' } ),
]
+ _test_no_pad = True
+
class OP_MODE(enum.IntEnum):
normal = 0x00
type_approval = 0x80
@@ -765,16 +637,15 @@ class EF_EST(EF_UServiceTable):
@with_default_category('File-Specific Commands')
class AddlShellCommands(CommandSet):
- def __init__(self):
- super().__init__()
-
def do_est_service_enable(self, arg):
- """Enable a service within EF.UST"""
- self._cmd.card.update_est(int(arg), 1)
+ """Enable a service within EF.EST"""
+ selected_file = self._cmd.lchan.selected_file
+ selected_file.ust_update(self._cmd, [int(arg)], [])
def do_est_service_disable(self, arg):
- """Disable a service within EF.UST"""
- self._cmd.card.update_est(int(arg), 0)
+ """Disable a service within EF.EST"""
+ selected_file = self._cmd.lchan.selected_file
+ selected_file.ust_update(self._cmd, [], [int(arg)])
# TS 31.102 Section 4.2.48
class EF_ACL(TransparentEF):
@@ -808,7 +679,7 @@ class EF_RPLMNAcT(TransRecEF):
def __init__(self, fid='6f65', sfid=None, name='EF.RPLMNAcTD', size=(2, 4), rec_len=2,
desc='RPLMN Last used Access Technology', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
- def _decode_record_hex(self, in_hex, **kwargs):
+ def _decode_record_hex(self, in_hex, **_kwargs):
return dec_act(in_hex)
# TODO: Encode
@@ -873,6 +744,7 @@ class EF_GBANL(LinFixedEF):
# TS 31.102 Section 4.2.85
class EF_EHPLMNPI(TransparentEF):
_test_de_encode = [
+ ( '00', {"presentation_ind": "no_preference"} ),
( '02', { "presentation_ind": "display_all" } ),
]
def __init__(self, fid='6fdb', sfid=None, name='EF.EHPLMNPI', size=(1, 1),
@@ -980,30 +852,50 @@ class EF_IPS(CyclicEF):
# TS 31.102 Section 4.2.103
class EF_ePDGId(TransparentEF):
- class ePDGId(BER_TLV_IE, tag=0x80, nested=[]):
+ _test_de_encode = [
+ ( '801100657064672e6f736d6f636f6d2e6f7267', {'e_pdg_id': {"type_of_ePDG_address": "FQDN", "ePDG_address" : "epdg.osmocom.org" } } ),
+ ( '800501c0a8a001', {'e_pdg_id': {"type_of_ePDG_address": "IPv4", "ePDG_address" : "192.168.160.1" } } ),
+ ( '80110220010db8000000000000000000000023', {'e_pdg_id': {"type_of_ePDG_address": "IPv6", "ePDG_address" : "2001:db8::23" } } ),
+ ]
+ class ePDGId(BER_TLV_IE, tag=0x80):
_construct = Struct('type_of_ePDG_address'/Enum(Byte, FQDN=0, IPv4=1, IPv6=2),
- 'ePDG_address'/Switch(this.type_of_address,
- {'FQDN': GreedyString("utf8"),
- 'IPv4': HexAdapter(GreedyBytes),
- 'IPv6': HexAdapter(GreedyBytes)}))
+ 'ePDG_address'/Switch(this.type_of_ePDG_address,
+ {'FQDN': Utf8Adapter(GreedyBytes),
+ 'IPv4': Ipv4Adapter(GreedyBytes),
+ 'IPv6': Ipv6Adapter(GreedyBytes)}))
- def __init__(self, fid='6ff3', sfid=None, name='EF.eDPDGId', desc='Home ePDG Identifier', **kwargs):
+ def __init__(self, fid='6ff3', sfid=None, name='EF.ePDGId', desc='Home ePDG Identifier', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
self._tlv = EF_ePDGId.ePDGId
+# TS 31.102 Section 4.2.104
+class EF_ePDGSelection(TransparentEF):
+ _test_de_encode = [
+ ( '800600f110000100', {'e_pdg_selection': [{'plmn': '001-01', 'epdg_priority': 1, 'epdg_fqdn_format': 'operator_identified' }] }),
+ ( '800600011000a001', {'e_pdg_selection': [{'plmn': '001-001', 'epdg_priority': 160, 'epdg_fqdn_format': 'location_based' }] }),
+ ]
+ class ePDGSelection(BER_TLV_IE, tag=0x80):
+ _construct = GreedyRange(Struct('plmn'/PlmnAdapter(Bytes(3)),
+ 'epdg_priority'/Int16ub,
+ 'epdg_fqdn_format'/Enum(Int8ub, operator_identified=0, location_based=1)))
+
+ def __init__(self, fid='6ff4', sfid=None, name='EF.ePDGSelection', desc='ePDG Selection Information', **kwargs):
+ super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
+ self._tlv = EF_ePDGSelection.ePDGSelection
+
# TS 31.102 Section 4.2.106
class EF_FromPreferred(TransparentEF):
def __init__(self, fid='6ff7', sfid=None, name='EF.FromPreferred', size=(1, 1),
desc='From Preferred', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
- self._construct = BitStruct('rfu'/BitsRFU(7), 'from_preferred'/Bit)
+ self._construct = BitStruct('rfu'/BitsRFU(7), 'from_preferred'/Flag)
# TS 31.102 Section 4.2.114
class EF_eAKA(TransparentEF):
def __init__(self, fid='6f01', sfid=None, name='EF.eAKA', size=(1, 1),
desc='enhanced AKA support', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
- self._construct = BitStruct('rfu'/BitsRFU(7), 'enhanced_sqn_calculation_supported'/Bit)
+ self._construct = BitStruct('rfu'/BitsRFU(7), 'enhanced_sqn_calculation_supported'/Flag)
######################################################################
@@ -1023,6 +915,83 @@ class DF_GSM_ACCESS(CardDF):
######################################################################
+# DF.NHB
+######################################################################
+
+# 3GPP TS 31.102 Section 4.4.6.2
+class EF_ACSGL(LinFixedEF):
+ _test_de_encode = [
+ ( 'a00d800362f21081060000000002e0',
+ {'csg_list': [{'plmn': '262-01'},
+ {'csg_information': { 'csg_type': 'from_other_sources',
+ 'hnb_name_indication': 'from_other_sources',
+ 'csg_id': { 'id': 23 } } } ] } ),
+ ]
+ class Plmn(BER_TLV_IE, tag=0x80):
+ _construct = PlmnAdapter(Bytes(3))
+ class CsgInformation(BER_TLV_IE, tag=0x81):
+ _construct = Struct('csg_type'/Enum(Int8ub, from_other_sources=0),
+ 'hnb_name_indication'/Enum(Int8ub, from_other_sources=0),
+ 'csg_id'/BitStruct('id'/BitsInteger(27), Padding(5)))
+ class CsgList(BER_TLV_IE, tag=0xa0, nested=[Plmn, CsgInformation]):
+ pass
+ def __init__(self, fid='4f81', sfid=0x01, name='EF.ACSGL', desc='Allowed CSG Lists', service=86, **kwargs):
+ super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, service=service, rec_len=(1, None), **kwargs)
+ self._tlv = EF_ACSGL.CsgList
+
+# 3GPP TS 31.102 Section 4.4.6.3
+class EF_CSGT(LinFixedEF):
+ _test_de_encode = [
+ ( '8906810300666f6f', [{ 'text_csg_type': 'foo' }] ),
+ ( '8906810300666f6f801068747470733a2f2f666f6f2e6261722f',
+ [{ 'text_csg_type': 'foo' }, { "graphics_csg_type_uri": "https://foo.bar/" }] ),
+ ]
+ class TextCsgType(BER_TLV_IE, tag=0x89):
+ _construct = Ucs2Adapter(GreedyBytes)
+ class GraphicsCsgTypeURI(BER_TLV_IE, tag=0x80):
+ _construct = Utf8Adapter(GreedyBytes)
+ class GraphicsCsgTypeEfImg(BER_TLV_IE, tag=0x81):
+ _construct = Int8ub
+ class Csgt_TLV_Collection(TLV_IE_Collection,
+ nested=[TextCsgType, GraphicsCsgTypeURI, GraphicsCsgTypeEfImg]):
+ pass
+ def __init__(self, fid='4f82', sfid=0x02, name='EF.CSGT', desc='CSG Types', service=86, **kwargs):
+ super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, service=service, rec_len=(1, None), **kwargs)
+ self._tlv = EF_CSGT.Csgt_TLV_Collection
+
+
+# 3GPP TS 31.102 Section 4.4.6.4
+class EF_HNBN(LinFixedEF):
+ _test_de_encode = [
+ ( '800b8108006d61686c7a656974', { 'hnb_name': 'mahlzeit' }),
+ ]
+ class HnbName(BER_TLV_IE, tag=0x80):
+ _construct = Ucs2Adapter(GreedyBytes)
+ def __init__(self, fid='4f83', sfid=0x03, name='EF.HNBN', desc='Home NodeB Name', service=86, **kwargs):
+ super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, service=service, rec_len=(1, None), **kwargs)
+ self._tlv = EF_HNBN.HnbName
+
+# 3GPP TS 31.102 Section 4.4.6.5
+class EF_OCSGL(LinFixedEF):
+ _test_de_encode = [
+ ( 'a010800362f21081060000000002e0820100',
+ {'operator_csg_list': [{'plmn': '262-01'},
+ {'csg_information': { 'csg_type': 'from_other_sources',
+ 'hnb_name_indication': 'from_other_sources',
+ 'csg_id': { 'id': 23 } } },
+ {'csg_display_indicator': 'all_available_csg_ids' } ] } ),
+
+ ]
+ class CsgDisplayIndicator(BER_TLV_IE, tag=0x82):
+ _construct = Enum(Int8ub, all_available_csg_ids=0, only_ocsgl_csg_ids=1)
+ class OperatorCsgList(BER_TLV_IE, tag=0xa0, nested=[EF_ACSGL.Plmn, EF_ACSGL.CsgInformation, CsgDisplayIndicator]):
+ pass
+ def __init__(self, fid='4f84', sfid=0x04, name='EF.OCSGL', desc='Operator CSG Lists', service=90, **kwargs):
+ super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, service=service, rec_len=(1, None), **kwargs)
+ self._tlv = EF_OCSGL.OperatorCsgList
+
+
+######################################################################
# DF.5GS
######################################################################
@@ -1054,7 +1023,7 @@ class EF_UAC_AIC(TransparentEF):
class EF_OPL5G(LinFixedEF):
def __init__(self, fid='4f08', sfid=0x08, name='EF.OPL5G', desc='5GS Operator PLMN List', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=(10, None), **kwargs)
- Tai = Struct('mcc_mnc'/BcdAdapter(Bytes(3)), 'tac_min'/HexAdapter(Bytes(3)),
+ Tai = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)), 'tac_min'/HexAdapter(Bytes(3)),
'tac_max'/HexAdapter(Bytes(3)))
self._construct = Struct('tai'/Tai, 'pnn_record_id'/Int8ub)
@@ -1062,15 +1031,15 @@ class EF_OPL5G(LinFixedEF):
class EF_SUPI_NAI(TransparentEF):
class NetworkSpecificIdentifier(TLV_IE, tag=0x80):
# RFC 7542 encoded as UTF-8 string
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
class GlobalLineIdentifier(TLV_IE, tag=0x81):
# TS 23.003 clause 28.16.2
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
class GlobalCableIdentifier(TLV_IE, tag=0x82):
# TS 23.003 clause 28.15.2
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
class NAI_TLV_Collection(TLV_IE_Collection,
nested=[NetworkSpecificIdentifier, GlobalLineIdentifier, GlobalCableIdentifier]):
@@ -1095,7 +1064,7 @@ class EF_Routing_Indicator(TransparentEF):
# TS 31.102 Section 4.4.11.13
class EF_TN3GPPSNN(TransparentEF):
class ServingNetworkName(BER_TLV_IE, tag=0x80):
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='4f0c', sfid=0x0c, name='EF.TN3GPPSNN',
desc='Trusted non-3GPP Serving network names list', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -1166,16 +1135,15 @@ class EF_5G_PROSE_ST(EF_UServiceTable):
@with_default_category('File-Specific Commands')
class AddlShellCommands(CommandSet):
- def __init__(self):
- super().__init__()
-
def do_prose_service_activate(self, arg):
"""Activate a service within EF.5G_PROSE_ST"""
- self._cmd.card.update_ust(int(arg), 1)
+ selected_file = self._cmd.lchan.selected_file
+ selected_file.ust_update(self._cmd, [int(arg)], [])
def do_prose_service_deactivate(self, arg):
"""Deactivate a service within EF.5G_PROSE_ST"""
- self._cmd.card.update_ust(int(arg), 0)
+ selected_file = self._cmd.lchan.selected_file
+ selected_file.ust_update(self._cmd, [], [int(arg)])
# TS 31.102 Section 4.4.13.3 (Rel 17)
class EF_5G_PROSE_DD(TransparentEF):
@@ -1387,12 +1355,12 @@ class DF_HNB(CardDF):
def __init__(self, fid='5f50', name='DF.HNB', desc='Files for HomeNodeB purpose', **kwargs):
super().__init__(fid=fid, name=name, desc=desc, **kwargs)
files = [
- LinFixedEF('4f81', 0x01, 'EF.ACSGL', 'Allowed CSG Lists', service=86),
- LinFixedEF('4f82', 0x02, 'EF.CSGTL', 'CSG Types', service=86),
- LinFixedEF('4f83', 0x03, 'EF.HNBN', 'Home NodeB Name', service=86),
- LinFixedEF('4f84', 0x04, 'EF.OCSGL', 'Operator CSG Lists', service=90),
- LinFixedEF('4f85', 0x05, 'EF.OCSGT', 'Operator CSG Type', service=90),
- LinFixedEF('4f86', 0x06, 'EF.OHNBN', 'Operator Home NodeB Name', service=90),
+ EF_ACSGL(),
+ EF_CSGT(),
+ EF_HNBN(),
+ EF_OCSGL(),
+ EF_CSGT('4f85', 0x05, 'EF.OCSGT', 'Operator CSG Type', service=90),
+ EF_HNBN('4f86', 0x06, 'EF.OHNBN', 'Operator Home NodeB Name', service=90),
]
self.add_files(files)
@@ -1436,10 +1404,10 @@ class DF_USIM_5GS(CardDF):
# I'm looking at 31.102 R16.6
EF_5GS3GPPLOCI(service=122),
EF_5GS3GPPLOCI('4f02', 0x02, 'EF.5GSN3GPPLOCI',
- '5GS non-3GPP location information', service=122),
+ desc='5GS non-3GPP location information', service=122),
EF_5GS3GPPNSC(service=122),
EF_5GS3GPPNSC('4f04', 0x04, 'EF.5GSN3GPPNSC',
- '5GS non-3GPP Access NAS Security Context', service=122),
+ desc='5GS non-3GPP Access NAS Security Context', service=122),
EF_5GAUTHKEYS(service=123),
EF_UAC_AIC(service=126),
EF_SUCI_Calc_Info(service=124),
@@ -1447,7 +1415,7 @@ class DF_USIM_5GS(CardDF):
EF_SUPI_NAI(service=130),
EF_Routing_Indicator(service=124),
TransparentEF('4F0B', 0x0b, 'EF.URSP',
- 'UE Route Selector Policies per PLMN', service=132),
+ desc='UE Route Selector Policies per PLMN', service=132),
EF_TN3GPPSNN(service=133),
# Rel-17 additions below
EF_CAG(service=137),
@@ -1474,60 +1442,58 @@ class DF_SAIP(CardDF):
class ADF_USIM(CardADF):
- def __init__(self, aid='a0000000871002', name='ADF.USIM', fid=None, sfid=None,
- desc='USIM Application'):
- super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
+ def __init__(self, aid='a0000000871002', has_fs=True, name='ADF.USIM', fid=None, sfid=None,
+ desc='USIM Application', has_imsi=True):
+ super().__init__(aid=aid, has_fs=has_fs, fid=fid, sfid=sfid, name=name, desc=desc)
# add those commands to the general commands of a TransparentEF
self.shell_commands += [self.AddlShellCommands()]
files = [
EF_LI(sfid=0x02),
- EF_IMSI(sfid=0x07),
EF_Keys(),
EF_Keys('6f09', 0x09, 'EF.KeysPS',
desc='Ciphering and Integrity Keys for PS domain'),
EF_xPLMNwAcT('6f60', 0x0a, 'EF.PLMNwAcT',
- 'User controlled PLMN Selector with Access Technology', service=20),
+ desc='User controlled PLMN Selector with Access Technology', service=20),
EF_HPPLMN(),
EF_ACMmax(service=13),
EF_UST(),
CyclicEF('6f39', None, 'EF.ACM',
- 'Accumulated call meter', rec_len=(3, 3), service=13),
- TransparentEF('6f3e', None, 'EF.GID1', 'Group Identifier Level 1', service=17),
- TransparentEF('6f3f', None, 'EF.GID2', 'Group Identifier Level 2', service=18),
+ desc='Accumulated call meter', rec_len=(3, 3), service=13),
+ TransparentEF('6f3e', None, 'EF.GID1', desc='Group Identifier Level 1', service=17),
+ TransparentEF('6f3f', None, 'EF.GID2', desc='Group Identifier Level 2', service=18),
EF_SPN(service=19),
TransparentEF('6f41', None, 'EF.PUCT',
- 'Price per unit and currency table', size=(5, 5), service=13),
+ desc='Price per unit and currency table', size=(5, 5), service=13),
EF_CBMI(service=15),
EF_ACC(sfid=0x06),
- EF_PLMNsel('6f7b', 0x0d, 'EF.FPLMN',
- 'Forbidden PLMNs', size=(12, None)),
+ EF_PLMNsel('6f7b', 0x0d, 'EF.FPLMN', desc='Forbidden PLMNs', size=(12, None)),
EF_LOCI(),
EF_AD(),
EF_CBMID(sfid=0x0e, service=29),
EF_ECC(),
EF_CBMIR(service=16),
EF_PSLOCI(),
- EF_ADN('6f3b', None, 'EF.FDN', 'Fixed Dialling Numbers', service=[2, 89], ext=2),
+ EF_ADN('6f3b', None, 'EF.FDN', desc='Fixed Dialling Numbers', service=[2, 89], ext=2),
EF_SMS('6f3c', None, service=10),
EF_MSISDN(service=21),
EF_SMSP(service=12),
EF_SMSS(service=10),
- EF_ADN('6f49', None, 'EF.SDN', 'Service Dialling Numbers', service=[4, 89], ext=3),
- EF_EXT('6f4b', None, 'EF.EXT2', 'Extension2 (FDN)', service=3),
- EF_EXT('6f4c', None, 'EF.EXT3', 'Extension2 (SDN)', service=5),
+ EF_ADN('6f49', None, 'EF.SDN', desc='Service Dialling Numbers', service=[4, 89], ext=3),
+ EF_EXT('6f4b', None, 'EF.EXT2', desc='Extension2 (FDN)', service=3),
+ EF_EXT('6f4c', None, 'EF.EXT3', desc='Extension2 (SDN)', service=5),
EF_SMSR(service=11),
EF_ICI(service=9),
EF_OCI(service=8),
EF_ICT(service=9),
- EF_ICT('6f83', None, 'EF.OCT', 'Outgoing Call Timer', service=8),
- EF_EXT('6f4e', None, 'EF.EXT5', 'Extension5 (ICI/OCI/MSISDN)', service=44),
+ EF_ICT('6f83', None, 'EF.OCT', desc='Outgoing Call Timer', service=8),
+ EF_EXT('6f4e', None, 'EF.EXT5', desc='Extension5 (ICI/OCI/MSISDN)', service=44),
EF_CCP2(service=14),
EF_eMLPP(service=24),
EF_AAeM(service=25),
# EF_Hiddenkey
- EF_ADN('6f4d', None, 'EF.BDN', 'Barred Dialling Numbers', service=6, ext=4),
- EF_EXT('6f55', None, 'EF.EXT4', 'Extension4 (BDN/SSC)', service=7),
+ EF_ADN('6f4d', None, 'EF.BDN', desc='Barred Dialling Numbers', service=6, ext=4),
+ EF_EXT('6f55', None, 'EF.EXT4', desc='Extension4 (BDN/SSC)', service=7),
EF_CMI(service=6),
EF_EST(service=[2, 6, 34, 35]),
EF_ACL(service=35),
@@ -1535,56 +1501,60 @@ class ADF_USIM(CardADF):
EF_CNL(service=37),
EF_START_HFN(),
EF_THRESHOLD(),
- EF_xPLMNwAcT('6f61', 0x11, 'EF.OPLMNwAcT', 'User controlled PLMN Selector with Access Technology', service=42),
- EF_xPLMNwAcT('6f62', 0x13, 'EF.HPLMNwAcT', 'HPLMN Selector with Access Technology', service=43),
+ EF_xPLMNwAcT('6f61', 0x11, 'EF.OPLMNwAcT', desc='User controlled PLMN Selector with Access Technology', service=42),
+ EF_xPLMNwAcT('6f62', 0x13, 'EF.HPLMNwAcT', desc='HPLMN Selector with Access Technology', service=43),
EF_ARR('6f06', 0x17),
EF_RPLMNAcT(),
- TransparentEF('6fc4', None, 'EF.NETPAR', 'Network Parameters'),
+ TransparentEF('6fc4', None, 'EF.NETPAR', desc='Network Parameters'),
EF_PNN('6fc5', 0x19, service=45),
EF_OPL(service=46),
- EF_ADN('6fc7', None, 'EF.MBDN', 'Mailbox Dialling Numbers', service=47, ext=6),
- EF_EXT('6fc8', None, 'EF.EXT6', 'Extension6 (MBDN)'),
+ EF_ADN('6fc7', None, 'EF.MBDN', desc='Mailbox Dialling Numbers', service=47, ext=6),
+ EF_EXT('6fc8', None, 'EF.EXT6', desc='Extension6 (MBDN)'),
EF_MBI(service=47),
EF_MWIS(service=48),
- EF_ADN('6fcb', None, 'EF.CFIS', 'Call Forwarding Indication Status', service=49, ext=7),
- EF_EXT('6fcc', None, 'EF.EXT7', 'Extension7 (CFIS)'),
- TransparentEF('6fcd', None, 'EF.SPDI', 'Service Provider Display Information', service=51),
+ EF_CFIS(service=49, ext=7),
+ EF_EXT('6fcc', None, 'EF.EXT7', desc='Extension7 (CFIS)'),
+ TransparentEF('6fcd', None, 'EF.SPDI', desc='Service Provider Display Information', service=51),
EF_MMSN(service=52),
- EF_EXT('6fcf', None, 'EF.EXT8', 'Extension8 (MMSN)', service=53),
+ EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)', service=53),
EF_MMSICP(service=52),
EF_MMSUP(service=52),
EF_MMSUCP(service=(52, 55)),
EF_NIA(service=56, fid='6fd3'),
EF_VGCS(service=57),
EF_VGCSS(service=57),
- EF_VGCS('6fb3', None, 'EF.VBS', 'Voice Broadcast Service', service=58),
- EF_VGCSS('6fb4', None, 'EF.VBSS', 'Voice Broadcast Service Status', service=58),
+ EF_VGCS('6fb3', None, 'EF.VBS', desc='Voice Broadcast Service', service=58),
+ EF_VGCSS('6fb4', None, 'EF.VBSS', desc='Voice Broadcast Service Status', service=58),
EF_VGCSCA(service=64),
- EF_VGCSCA('6fd5', None, 'EF.VBCSCA', 'Voice Broadcast Service Ciphering Algorithm', service=65),
+ EF_VGCSCA('6fd5', None, 'EF.VBCSCA', desc='Voice Broadcast Service Ciphering Algorithm', service=65),
EF_GBABP(service=68),
EF_MSK(service=69),
EF_MUK(service=69),
EF_GBANL(service=68),
- EF_PLMNsel('6fd9', 0x1d, 'EF.EHPLMN', 'Equivalent HPLMN', size=(12, None), service=71),
+ EF_PLMNsel('6fd9', 0x1d, 'EF.EHPLMN', desc='Equivalent HPLMN', size=(12, None), service=71),
EF_EHPLMNPI(service=(71, 73)),
# EF_LRPLMNSI ('6fdc', service=74)
EF_NAFKCA(service=(68, 76)),
- TransparentEF('6fde', None, 'EF.SPNI', 'Service Provider Name Icon', service=78),
- LinFixedEF('6fdf', None, 'EF.PNNI', 'PLMN Network Name Icon', service=79),
+ TransparentEF('6fde', None, 'EF.SPNI', desc='Service Provider Name Icon', service=78),
+ LinFixedEF('6fdf', None, 'EF.PNNI', desc='PLMN Network Name Icon', service=79),
EF_NCP_IP(service=80),
- EF_EPSLOCI('6fe3', 0x1e, 'EF.EPSLOCI', 'EPS location information', service=85),
+ EF_EPSLOCI('6fe3', 0x1e, 'EF.EPSLOCI', desc='EPS location information', service=85),
EF_EPSNSC(service=85),
- TransparentEF('6fe6', None, 'EF.UFC', 'USAT Facility Control', size=(1, 16)),
- TransparentEF('6fe8', None, 'EF.NASCONFIG', 'Non Access Stratum Configuration', service=96),
+ # EF.UFC Test data: 801e60c01e900080040000000000000000f0000000004000000000000080
+ TransparentEF('6fe6', None, 'EF.UFC', desc='USAT Facility Control', size=(1, 16)),
+ TransparentEF('6fe8', None, 'EF.NASCONFIG', desc='Non Access Stratum Configuration', service=96),
# UICC IARI (only in cards that have no ISIM) service=95
EF_PWS(service=97),
- LinFixedEF('6fed', None, 'EF.FDNURI', 'Fixed Dialling Numbers URI', service=(2, 99)),
- LinFixedEF('6fee', None, 'EF.BDNURI', 'Barred Dialling Numbers URI', service=(6, 99)),
- LinFixedEF('6fef', None, 'EF.SDNURI', 'Service Dialling Numbers URI', service=(4, 99)),
+ LinFixedEF('6fed', None, 'EF.FDNURI', desc='Fixed Dialling Numbers URI', service=(2, 99)),
+ LinFixedEF('6fee', None, 'EF.BDNURI', desc='Barred Dialling Numbers URI', service=(6, 99)),
+ LinFixedEF('6fef', None, 'EF.SDNURI', desc='Service Dialling Numbers URI', service=(4, 99)),
# EF_IWL (IMEI(SV) White List)
EF_IPS(),
EF_ePDGId(service=(106, 107)),
- # FIXME: from EF_ePDGSelection onwards
+ EF_ePDGSelection(service=(106, 107)),
+ EF_ePDGId('6ff5', None, 'EF.ePDGIdEm', desc='Emergency ePDG Identifier', service=(110, 111)),
+ EF_ePDGSelection('6ff6', None, 'EF.ePDGSelectionEm',
+ desc='ePDG Selection Information for Emergency Services', service=(110, 111)),
EF_FromPreferred(service=114),
EF_eAKA(),
# FIXME: DF_SoLSA service=23
@@ -1600,6 +1570,10 @@ class ADF_USIM(CardADF):
DF_5G_ProSe(service=139),
DF_SAIP(),
]
+
+ if has_imsi:
+ files.append(EF_IMSI(sfid=0x07))
+
self.add_files(files)
def decode_select_response(self, data_hex):
@@ -1607,22 +1581,19 @@ class ADF_USIM(CardADF):
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
- def __init__(self):
- super().__init__()
-
authenticate_parser = argparse.ArgumentParser()
- authenticate_parser.add_argument('rand', help='Random challenge')
- authenticate_parser.add_argument('autn', help='Authentication Nonce')
+ authenticate_parser.add_argument('rand', type=is_hexstr, help='Random challenge')
+ authenticate_parser.add_argument('autn', type=is_hexstr, help='Authentication Nonce')
#authenticate_parser.add_argument('--context', help='Authentication context', default='3G')
@cmd2.with_argparser(authenticate_parser)
def do_authenticate(self, opts):
"""Perform Authentication and Key Agreement (AKA)."""
- (data, sw) = self._cmd.card._scc.authenticate(opts.rand, opts.autn)
+ (data, _sw) = self._cmd.lchan.scc.authenticate(opts.rand, opts.autn)
self._cmd.poutput_json(data)
term_prof_parser = argparse.ArgumentParser()
- term_prof_parser.add_argument('PROFILE', help='Hexstring of encoded terminal profile')
+ term_prof_parser.add_argument('PROFILE', type=is_hexstr, help='Hexstring of encoded terminal profile')
@cmd2.with_argparser(term_prof_parser)
def do_terminal_profile(self, opts):
@@ -1632,11 +1603,11 @@ class ADF_USIM(CardADF):
in the context of SIM Toolkit, Proactive SIM and OTA. You
must specify a hex-string with the encoded terminal profile
you want to send to the card."""
- (data, sw) = self._cmd.card._scc.terminal_profile(opts.PROFILE)
+ (data, sw) = self._cmd.lchan.scc.terminal_profile(opts.PROFILE)
self._cmd.poutput('SW: %s, data: %s' % (sw, data))
envelope_parser = argparse.ArgumentParser()
- envelope_parser.add_argument('PAYLOAD', help='Hexstring of encoded payload to ENVELOPE')
+ envelope_parser.add_argument('PAYLOAD', type=is_hexstr, help='Hexstring of encoded payload to ENVELOPE')
@cmd2.with_argparser(envelope_parser)
def do_envelope(self, opts):
@@ -1644,11 +1615,11 @@ class ADF_USIM(CardADF):
variety of information is communicated from the terminal
(modem/phone) to the card, particularly in the context of
SIM Toolkit, Proactive SIM and OTA."""
- (data, sw) = self._cmd.card._scc.envelope(opts.PAYLOAD)
+ (data, sw) = self._cmd.lchan.scc.envelope(opts.PAYLOAD)
self._cmd.poutput('SW: %s, data: %s' % (sw, data))
envelope_sms_parser = argparse.ArgumentParser()
- envelope_sms_parser.add_argument('TPDU', help='Hexstring of encoded SMS TPDU')
+ envelope_sms_parser.add_argument('TPDU', type=is_hexstr, help='Hexstring of encoded SMS TPDU')
@cmd2.with_argparser(envelope_sms_parser)
def do_envelope_sms(self, opts):
@@ -1662,7 +1633,7 @@ class ADF_USIM(CardADF):
dev_ids = DeviceIdentities(
decoded={'source_dev_id': 'network', 'dest_dev_id': 'uicc'})
sms_dl = SMSPPDownload(children=[dev_ids, tpdu_ie])
- (data, sw) = self._cmd.card._scc.envelope(b2h(sms_dl.to_tlv()))
+ (data, sw) = self._cmd.lchan.scc.envelope(b2h(sms_dl.to_tlv()))
self._cmd.poutput('SW: %s, data: %s' % (sw, data))
get_id_parser = argparse.ArgumentParser()
@@ -1676,7 +1647,7 @@ class ADF_USIM(CardADF):
context = 0x01 # SUCI
if opts.nswo_context:
context = 0x02 # SUCI 5G NSWO
- (data, sw) = self._cmd.card._scc.get_identity(context)
+ (data, _sw) = self._cmd.lchan.scc.get_identity(context)
do = SUCI_TlvDataObject()
do.from_tlv(h2b(data))
do_d = do.to_dict()
@@ -1698,3 +1669,10 @@ sw_usim = {
class CardApplicationUSIM(CardApplication):
def __init__(self):
super().__init__('USIM', adf=ADF_USIM(), sw=sw_usim)
+
+# TS 31.102 Annex N + TS 102 220 Annex E
+class CardApplicationUSIMnonIMSI(CardApplication):
+ def __init__(self):
+ adf = ADF_USIM(aid='a000000087100b', name='ADF.USIM-non-IMSI', has_imsi=False,
+ desc='3GPP USIM (non-IMSI SUPI Type) - TS 31.102 Annex N')
+ super().__init__('USIM-non-IMSI', adf=adf, sw=sw_usim)
diff --git a/pySim/ts_31_102_telecom.py b/pySim/ts_31_102_telecom.py
index a0098c7..962b7dd 100644
--- a/pySim/ts_31_102_telecom.py
+++ b/pySim/ts_31_102_telecom.py
@@ -26,11 +26,12 @@ Needs to be a separate python module to avoid cyclic imports
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
+from construct import Optional as COptional
+from construct import Struct, Int16ub, Int32ub
+
from pySim.tlv import *
from pySim.filesystem import *
from pySim.construct import *
-from construct import Optional as COptional
-from construct import *
# TS 31.102 Section 4.2.8
class EF_UServiceTable(TransparentEF):
@@ -42,7 +43,7 @@ class EF_UServiceTable(TransparentEF):
def _bit_byte_offset_for_service(service: int) -> Tuple[int, int]:
i = service - 1
byte_offset = i//8
- bit_offset = (i % 8)
+ bit_offset = i % 8
return (byte_offset, bit_offset)
def _decode_bin(self, in_bin):
@@ -73,7 +74,7 @@ class EF_UServiceTable(TransparentEF):
service_nr = int(srv)
(byte_offset, bit_offset) = EF_UServiceTable._bit_byte_offset_for_service(
service_nr)
- if in_json[srv]['activated'] == True:
+ if in_json[srv]['activated'] is True:
bit = 1
else:
bit = 0
@@ -82,7 +83,7 @@ class EF_UServiceTable(TransparentEF):
def get_active_services(self, cmd):
# obtain list of currently active services
- (service_data, sw) = cmd.lchan.read_binary_dec()
+ (service_data, _sw) = cmd.lchan.read_binary_dec()
active_services = []
for s in service_data.keys():
if service_data[s]['activated']:
@@ -120,7 +121,26 @@ class EF_UServiceTable(TransparentEF):
cmd.lchan.select_file(self)
return num_problems
-
+ def ust_update(self, cmd, activate=[], deactivate=[]):
+ service_data, _sw = cmd.lchan.read_binary()
+ service_data = h2b(service_data)
+
+ for service in activate:
+ nbyte, nbit = EF_UServiceTable._bit_byte_offset_for_service(service)
+ if nbyte > len(service_data):
+ missing = nbyte - service_data
+ service_data.extend(missing * "00")
+ service_data[nbyte] |= (1 << nbit)
+
+ for service in deactivate:
+ nbyte, nbit = EF_UServiceTable._bit_byte_offset_for_service(service)
+ if nbyte > len(service_data):
+ missing = nbyte - service_data
+ service_data.extend(missing * "00")
+ service_data[nbyte] &= ~(1 << nbit)
+
+ service_data = b2h(service_data)
+ cmd.lchan.update_binary(service_data)
# TS 31.102 Section 4.4.2.1
class EF_PBR(LinFixedEF):
diff --git a/pySim/ts_31_103.py b/pySim/ts_31_103.py
index f1c8dd7..81de13c 100644
--- a/pySim/ts_31_103.py
+++ b/pySim/ts_31_103.py
@@ -22,6 +22,7 @@ Various constants from 3GPP TS 31.103 V16.1.0
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
+from construct import Struct, Switch, this, Bytes, GreedyString
from pySim.filesystem import *
from pySim.utils import *
from pySim.tlv import *
@@ -30,6 +31,7 @@ from pySim.ts_31_102 import ADF_USIM, EF_FromPreferred
from pySim.ts_31_102_telecom import EF_UServiceTable
import pySim.ts_102_221
from pySim.ts_102_221 import EF_ARR
+from pySim.construct import *
# Mapping between ISIM Service Number and its description
EF_IST_map = {
@@ -56,36 +58,15 @@ EF_IST_map = {
21: 'MuD and MiD configuration data',
}
-EF_ISIM_ADF_map = {
- 'IST': '6F07',
- 'IMPI': '6F02',
- 'DOMAIN': '6F03',
- 'IMPU': '6F04',
- 'AD': '6FAD',
- 'ARR': '6F06',
- 'PCSCF': '6F09',
- 'GBAP': '6FD5',
- 'GBANL': '6FD7',
- 'NAFKCA': '6FDD',
- 'UICCIARI': '6FE7',
- 'SMS': '6F3C',
- 'SMSS': '6F43',
- 'SMSR': '6F47',
- 'SMSP': '6F42',
- 'FromPreferred': '6FF7',
- 'IMSConfigData': '6FF8',
- 'XCAPConfigData': '6FFC',
- 'WebRTCURI': '6FFA'
-}
-
# TS 31.103 Section 4.2.2
class EF_IMPI(TransparentEF):
_test_de_encode = [
( '803137333830303630303030303031303140696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267',
{ "nai": "738006000000101@ims.mnc000.mcc738.3gppnetwork.org" } ),
]
+
class nai(BER_TLV_IE, tag=0x80):
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6f02', sfid=0x02, name='EF.IMPI', desc='IMS private user identity', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -98,7 +79,7 @@ class EF_DOMAIN(TransparentEF):
{ "domain": "ims.mnc000.mcc738.3gppnetwork.org" } ),
]
class domain(BER_TLV_IE, tag=0x80):
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6f03', sfid=0x05, name='EF.DOMAIN', desc='Home Network Domain Name', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -111,7 +92,7 @@ class EF_IMPU(LinFixedEF):
{ "impu": "sip:738006000000101@ims.mnc000.mcc738.3gppnetwork.org" } ),
]
class impu(BER_TLV_IE, tag=0x80):
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6f04', sfid=0x04, name='EF.IMPU', desc='IMS public user identity', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -131,11 +112,13 @@ class EF_IST(EF_UServiceTable):
def do_ist_service_activate(self, arg):
"""Activate a service within EF.IST"""
- self._cmd.card.update_ist(int(arg), 1)
+ selected_file = self._cmd.lchan.selected_file
+ selected_file.ust_update(self._cmd, [int(arg)], [])
def do_ist_service_deactivate(self, arg):
"""Deactivate a service within EF.IST"""
- self._cmd.card.update_ist(int(arg), 0)
+ selected_file = self._cmd.lchan.selected_file
+ selected_file.ust_update(self._cmd, [], [int(arg)])
def do_ist_service_check(self, arg):
"""Check consistency between services of this file and files present/activated.
@@ -153,41 +136,63 @@ class EF_IST(EF_UServiceTable):
class EF_PCSCF(LinFixedEF):
_test_de_encode = [
( '802c0070637363662e696d732e6d6e633030302e6d63633733382e7075622e336770706e6574776f726b2e6f7267',
- { "addr": "pcscf.ims.mnc000.mcc738.pub.3gppnetwork.org", "addr_type": "00" } ),
+ {'pcscf_address': { "address": "pcscf.ims.mnc000.mcc738.pub.3gppnetwork.org", "type_of_address": "FQDN" } } ),
+ ( '800501c0a80c22',
+ {'pcscf_address': { "address": "192.168.12.34", "type_of_address": "IPv4" } } ),
+ ( '801102fe800000000000000042d7fffe530335',
+ {'pcscf_address': { "address": "fe80::42:d7ff:fe53:335", "type_of_address": "IPv6" } } ),
]
+ class PcscfAddress(BER_TLV_IE, tag=0x80):
+ _construct = Struct('type_of_address'/Enum(Byte, FQDN=0, IPv4=1, IPv6=2),
+ 'address'/Switch(this.type_of_address,
+ {'FQDN': Utf8Adapter(GreedyBytes),
+ 'IPv4': Ipv4Adapter(GreedyBytes),
+ 'IPv6': Ipv6Adapter(GreedyBytes)}))
+
def __init__(self, fid='6f09', sfid=None, name='EF.P-CSCF', desc='P-CSCF Address', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
-
- def _decode_record_hex(self, raw_hex, **kwargs):
- addr, addr_type = dec_addr_tlv(raw_hex)
- return {"addr": addr, "addr_type": addr_type}
-
- def _encode_record_hex(self, json_in, **kwargs):
- addr = json_in['addr']
- addr_type = json_in['addr_type']
- return enc_addr_tlv(addr, addr_type)
+ self._tlv = EF_PCSCF.PcscfAddress
# TS 31.103 Section 4.2.9
class EF_GBABP(TransparentEF):
def __init__(self, fid='6fd5', sfid=None, name='EF.GBABP', desc='GBA Bootstrapping', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
+ self._construct = Struct('rand'/LV,
+ 'b_tid'/LV,
+ 'key_lifetime'/LV)
# TS 31.103 Section 4.2.10
class EF_GBANL(LinFixedEF):
+ class NAF_ID(BER_TLV_IE, tag=0x80):
+ _construct = Struct('fqdn'/Utf8Adapter(Bytes(this._.total_len-5)),
+ 'ua_spi'/HexAdapter(Bytes(5)))
+ class B_TID(BER_TLV_IE, tag=0x81):
+ _construct = Utf8Adapter(GreedyBytes)
+ # pylint: disable=undefined-variable
+ class GbaNlCollection(TLV_IE_Collection, nested=[NAF_ID, B_TID]):
+ pass
def __init__(self, fid='6fd7', sfid=None, name='EF.GBANL', desc='GBA NAF List', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
+ self._tlv = EF_GBANL.GbaNlCollection
# TS 31.103 Section 4.2.11
class EF_NAFKCA(LinFixedEF):
- # TODO: 80296273662e696d732e6d6e633030302e6d63633733382e7075622e336770706e6574776f726b2e6f7267ffffffffffffff
- # TODO: 8030656e65746e61667830312e696d732e6d6e633030302e6d63633733382e7075622e336770706e6574776f726b2e6f7267
+ _test_de_encode = [
+ ( '80296273662e696d732e6d6e633030302e6d63633733382e7075622e336770706e6574776f726b2e6f7267',
+ { 'naf_key_centre_address': 'bsf.ims.mnc000.mcc738.pub.3gppnetwork.org' } ),
+ ( '8030656e65746e61667830312e696d732e6d6e633030302e6d63633733382e7075622e336770706e6574776f726b2e6f7267',
+ { 'naf_key_centre_address': 'enetnafx01.ims.mnc000.mcc738.pub.3gppnetwork.org' }),
+ ]
+ class NafKeyCentreAddress(BER_TLV_IE, tag=0x80):
+ _construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6fdd', sfid=None, name='EF.NAFKCA', desc='NAF Key Centre Address', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
+ self._tlv = EF_NAFKCA.NafKeyCentreAddress
# TS 31.103 Section 4.2.16
class EF_UICCIARI(LinFixedEF):
class iari(BER_TLV_IE, tag=0x80):
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6fe7', sfid=None, name='EF.UICCIARI', desc='UICC IARI', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -254,7 +259,7 @@ class EF_XCAPConfigData(BerTlvEF):
# TS 31.103 Section 4.2.20
class EF_WebRTCURI(TransparentEF):
class uri(BER_TLV_IE, tag=0x80):
- _construct = GreedyString("utf8")
+ _construct = Utf8Adapter(GreedyBytes)
def __init__(self, fid='6ffa', sfid=None, name='EF.WebRTCURI', desc='WebRTC URI', **kwargs):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, **kwargs)
@@ -276,9 +281,9 @@ class EF_MuDMiDConfigData(BerTlvEF):
class ADF_ISIM(CardADF):
- def __init__(self, aid='a0000000871004', name='ADF.ISIM', fid=None, sfid=None,
+ def __init__(self, aid='a0000000871004', has_fs=True, name='ADF.ISIM', fid=None, sfid=None,
desc='ISIM Application'):
- super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
+ super().__init__(aid=aid, has_fs=has_fs, fid=fid, sfid=sfid, name=name, desc=desc)
files = [
EF_IMPI(),
diff --git a/pySim/ts_31_104.py b/pySim/ts_31_104.py
index 0adafba..239acbf 100644
--- a/pySim/ts_31_104.py
+++ b/pySim/ts_31_104.py
@@ -30,9 +30,9 @@ from pySim.ts_102_221 import EF_ARR
class ADF_HPSIM(CardADF):
- def __init__(self, aid='a000000087100A', name='ADF.HPSIM', fid=None, sfid=None,
+ def __init__(self, aid='a000000087100A', has_fs=True, name='ADF.HPSIM', fid=None, sfid=None,
desc='HPSIM Application'):
- super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
+ super().__init__(aid=aid, has_fs=has_fs, fid=fid, sfid=sfid, name=name, desc=desc)
files = [
EF_ARR(fid='6f06', sfid=0x06),
diff --git a/pySim/ts_51_011.py b/pySim/ts_51_011.py
index 700fb02..9b56b0e 100644
--- a/pySim/ts_51_011.py
+++ b/pySim/ts_51_011.py
@@ -30,9 +30,10 @@ order to describe the files specified in the relevant ETSI + 3GPP specifications
#
from pySim.profile import match_sim
-from pySim.profile import CardProfile
+from pySim.profile import CardProfile, CardProfileAddon
from pySim.filesystem import *
from pySim.ts_31_102_telecom import DF_PHONEBOOK, DF_MULTIMEDIA, DF_MCS, DF_V2X
+from pySim.gsm_r import AddonGSMR
import enum
from pySim.construct import *
from construct import Optional as COptional
@@ -41,236 +42,6 @@ from struct import pack, unpack
from typing import Tuple
from pySim.tlv import *
from pySim.utils import *
-MF_num = '3F00'
-
-DF_num = {
- 'TELECOM': '7F10',
-
- 'GSM': '7F20',
- 'IS-41': '7F22',
- 'FP-CTS': '7F23',
-
- 'GRAPHICS': '5F50',
-
- 'IRIDIUM': '5F30',
- 'GLOBST': '5F31',
- 'ICO': '5F32',
- 'ACeS': '5F33',
-
- 'EIA/TIA-553': '5F40',
- 'CTS': '5F60',
- 'SOLSA': '5F70',
-
- 'MExE': '5F3C',
-}
-
-EF_num = {
- # MF
- 'ICCID': '2FE2',
- 'ELP': '2F05',
- 'DIR': '2F00',
-
- # DF_TELECOM
- 'ADN': '6F3A',
- 'FDN': '6F3B',
- 'SMS': '6F3C',
- 'CCP': '6F3D',
- 'MSISDN': '6F40',
- 'SMSP': '6F42',
- 'SMSS': '6F43',
- 'LND': '6F44',
- 'SMSR': '6F47',
- 'SDN': '6F49',
- 'EXT1': '6F4A',
- 'EXT2': '6F4B',
- 'EXT3': '6F4C',
- 'BDN': '6F4D',
- 'EXT4': '6F4E',
- 'CMI': '6F58',
- 'ECCP': '6F4F',
-
- # DF_GRAPHICS
- 'IMG': '4F20',
-
- # DF_SoLSA
- 'SAI': '4F30',
- 'SLL': '4F31',
-
- # DF_MExE
- 'MExE-ST': '4F40',
- 'ORPK': '4F41',
- 'ARPK': '4F42',
- 'TPRPK': '4F43',
-
- # DF_GSM
- 'LP': '6F05',
- 'IMSI': '6F07',
- 'Kc': '6F20',
- 'DCK': '6F2C',
- 'PLMNsel': '6F30',
- 'HPPLMN': '6F31',
- 'CNL': '6F32',
- 'ACMmax': '6F37',
- 'SST': '6F38',
- 'ACM': '6F39',
- 'GID1': '6F3E',
- 'GID2': '6F3F',
- 'PUCT': '6F41',
- 'CBMI': '6F45',
- 'SPN': '6F46',
- 'CBMID': '6F48',
- 'BCCH': '6F74',
- 'ACC': '6F78',
- 'FPLMN': '6F7B',
- 'LOCI': '6F7E',
- 'AD': '6FAD',
- 'PHASE': '6FAE',
- 'VGCS': '6FB1',
- 'VGCSS': '6FB2',
- 'VBS': '6FB3',
- 'VBSS': '6FB4',
- 'eMLPP': '6FB5',
- 'AAeM': '6FB6',
- 'ECC': '6FB7',
- 'CBMIR': '6F50',
- 'NIA': '6F51',
- 'KcGPRS': '6F52',
- 'LOCIGPRS': '6F53',
- 'SUME': '6F54',
- 'PLMNwAcT': '6F60',
- 'OPLMNwAcT': '6F61',
- # Figure 8 names it HPLMNAcT, but in the text it's names it HPLMNwAcT
- 'HPLMNAcT': '6F62',
- 'HPLMNwAcT': '6F62',
- 'CPBCCH': '6F63',
- 'INVSCAN': '6F64',
- 'PNN': '6FC5',
- 'OPL': '6FC6',
- 'MBDN': '6FC7',
- 'EXT6': '6FC8',
- 'MBI': '6FC9',
- 'MWIS': '6FCA',
- 'CFIS': '6FCB',
- 'EXT7': '6FCC',
- 'SPDI': '6FCD',
- 'MMSN': '6FCE',
- 'EXT8': '6FCF',
- 'MMSICP': '6FD0',
- 'MMSUP': '6FD1',
- 'MMSUCP': '6FD2',
-}
-
-DF = {
- 'TELECOM': [MF_num, DF_num['TELECOM']],
-
- 'GSM': [MF_num, DF_num['GSM']],
- 'IS-41': [MF_num, DF_num['IS-41']],
- 'FP-CTS': [MF_num, DF_num['FP-CTS']],
-
- 'GRAPHICS': [MF_num, DF_num['GRAPHICS']],
-
- 'IRIDIUM': [MF_num, DF_num['IRIDIUM']],
- 'GLOBST': [MF_num, DF_num['GLOBST']],
- 'ICO': [MF_num, DF_num['ICO']],
- 'ACeS': [MF_num, DF_num['ACeS']],
-
- 'EIA/TIA-553': [MF_num, DF_num['EIA/TIA-553']],
- 'CTS': [MF_num, DF_num['CTS']],
- 'SoLSA': [MF_num, DF_num['SOLSA']],
-
- 'MExE': [MF_num, DF_num['MExE']],
-}
-
-
-EF = {
- 'ICCID': [MF_num, EF_num['ICCID']],
- 'ELP': [MF_num, EF_num['ELP']],
- 'DIR': [MF_num, EF_num['DIR']],
-
- 'ADN': DF['TELECOM']+[EF_num['ADN']],
- 'FDN': DF['TELECOM']+[EF_num['FDN']],
- 'SMS': DF['TELECOM']+[EF_num['SMS']],
- 'CCP': DF['TELECOM']+[EF_num['CCP']],
- 'MSISDN': DF['TELECOM']+[EF_num['MSISDN']],
- 'SMSP': DF['TELECOM']+[EF_num['SMSP']],
- 'SMSS': DF['TELECOM']+[EF_num['SMSS']],
- 'LND': DF['TELECOM']+[EF_num['LND']],
- 'SMSR': DF['TELECOM']+[EF_num['SMSR']],
- 'SDN': DF['TELECOM']+[EF_num['SDN']],
- 'EXT1': DF['TELECOM']+[EF_num['EXT1']],
- 'EXT2': DF['TELECOM']+[EF_num['EXT2']],
- 'EXT3': DF['TELECOM']+[EF_num['EXT3']],
- 'BDN': DF['TELECOM']+[EF_num['BDN']],
- 'EXT4': DF['TELECOM']+[EF_num['EXT4']],
- 'CMI': DF['TELECOM']+[EF_num['CMI']],
- 'ECCP': DF['TELECOM']+[EF_num['ECCP']],
-
- 'IMG': DF['GRAPHICS']+[EF_num['IMG']],
-
- 'SAI': DF['SoLSA']+[EF_num['SAI']],
- 'SLL': DF['SoLSA']+[EF_num['SLL']],
-
- 'MExE-ST': DF['MExE']+[EF_num['MExE-ST']],
- 'ORPK': DF['MExE']+[EF_num['ORPK']],
- 'ARPK': DF['MExE']+[EF_num['ARPK']],
- 'TPRPK': DF['MExE']+[EF_num['TPRPK']],
-
- 'LP': DF['GSM']+[EF_num['LP']],
- 'IMSI': DF['GSM']+[EF_num['IMSI']],
- 'Kc': DF['GSM']+[EF_num['Kc']],
- 'DCK': DF['GSM']+[EF_num['DCK']],
- 'PLMNsel': DF['GSM']+[EF_num['PLMNsel']],
- 'HPPLMN': DF['GSM']+[EF_num['HPPLMN']],
- 'CNL': DF['GSM']+[EF_num['CNL']],
- 'ACMmax': DF['GSM']+[EF_num['ACMmax']],
- 'SST': DF['GSM']+[EF_num['SST']],
- 'ACM': DF['GSM']+[EF_num['ACM']],
- 'GID1': DF['GSM']+[EF_num['GID1']],
- 'GID2': DF['GSM']+[EF_num['GID2']],
- 'PUCT': DF['GSM']+[EF_num['PUCT']],
- 'CBMI': DF['GSM']+[EF_num['CBMI']],
- 'SPN': DF['GSM']+[EF_num['SPN']],
- 'CBMID': DF['GSM']+[EF_num['CBMID']],
- 'BCCH': DF['GSM']+[EF_num['BCCH']],
- 'ACC': DF['GSM']+[EF_num['ACC']],
- 'FPLMN': DF['GSM']+[EF_num['FPLMN']],
- 'LOCI': DF['GSM']+[EF_num['LOCI']],
- 'AD': DF['GSM']+[EF_num['AD']],
- 'PHASE': DF['GSM']+[EF_num['PHASE']],
- 'VGCS': DF['GSM']+[EF_num['VGCS']],
- 'VGCSS': DF['GSM']+[EF_num['VGCSS']],
- 'VBS': DF['GSM']+[EF_num['VBS']],
- 'VBSS': DF['GSM']+[EF_num['VBSS']],
- 'eMLPP': DF['GSM']+[EF_num['eMLPP']],
- 'AAeM': DF['GSM']+[EF_num['AAeM']],
- 'ECC': DF['GSM']+[EF_num['ECC']],
- 'CBMIR': DF['GSM']+[EF_num['CBMIR']],
- 'NIA': DF['GSM']+[EF_num['NIA']],
- 'KcGPRS': DF['GSM']+[EF_num['KcGPRS']],
- 'LOCIGPRS': DF['GSM']+[EF_num['LOCIGPRS']],
- 'SUME': DF['GSM']+[EF_num['SUME']],
- 'PLMNwAcT': DF['GSM']+[EF_num['PLMNwAcT']],
- 'OPLMNwAcT': DF['GSM']+[EF_num['OPLMNwAcT']],
- # Figure 8 names it HPLMNAcT, but in the text it's names it HPLMNwAcT
- 'HPLMNAcT': DF['GSM']+[EF_num['HPLMNAcT']],
- 'HPLMNwAcT': DF['GSM']+[EF_num['HPLMNAcT']],
- 'CPBCCH': DF['GSM']+[EF_num['CPBCCH']],
- 'INVSCAN': DF['GSM']+[EF_num['INVSCAN']],
- 'PNN': DF['GSM']+[EF_num['PNN']],
- 'OPL': DF['GSM']+[EF_num['OPL']],
- 'MBDN': DF['GSM']+[EF_num['MBDN']],
- 'EXT6': DF['GSM']+[EF_num['EXT6']],
- 'MBI': DF['GSM']+[EF_num['MBI']],
- 'MWIS': DF['GSM']+[EF_num['MWIS']],
- 'CFIS': DF['GSM']+[EF_num['CFIS']],
- 'EXT7': DF['GSM']+[EF_num['EXT7']],
- 'SPDI': DF['GSM']+[EF_num['SPDI']],
- 'MMSN': DF['GSM']+[EF_num['MMSN']],
- 'EXT8': DF['GSM']+[EF_num['EXT8']],
- 'MMSICP': DF['GSM']+[EF_num['MMSICP']],
- 'MMSUP': DF['GSM']+[EF_num['MMSUP']],
- 'MMSUCP': DF['GSM']+[EF_num['MMSUCP']],
-}
# Mapping between SIM Service Number and its description
EF_SST_map = {
@@ -364,12 +135,18 @@ class EF_ADN(LinFixedEF):
"unknown", "numbering_plan_id":
"isdn_e164" }, "dialing_nr":
"6082658001", "cap_conf_id": 255, "ext1_record_id": 255 }),
- ]
+ ( '4B756E64656E626574726575756E67FFFFFF0791947112122721ffffffffffff',
+ {"alpha_id": "Kundenbetreuung", "len_of_bcd": 7, "ton_npi": {"ext": True, "type_of_number":
+ "international",
+ "numbering_plan_id": "isdn_e164"},
+ "dialing_nr": "491721217212", "cap_conf_id": 255, "ext1_record_id": 255} )
+ ]
+ _test_no_pad = True
def __init__(self, fid='6f3a', sfid=None, name='EF.ADN', desc='Abbreviated Dialing Numbers', ext=1, **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(14, 30), **kwargs)
ext_name = 'ext%u_record_id' % ext
- self._construct = Struct('alpha_id'/COptional(GsmStringAdapter(Rpad(Bytes(this._.total_len-14)), codec='ascii')),
+ self._construct = Struct('alpha_id'/COptional(GsmOrUcs2Adapter(Rpad(Bytes(this._.total_len-14)))),
'len_of_bcd'/Int8ub,
'ton_npi'/TonNpi,
'dialing_nr'/ExtendedBcdAdapter(BcdAdapter(Rpad(Bytes(10)))),
@@ -410,7 +187,7 @@ class EF_SMS(LinFixedEF):
# TS 51.011 Section 10.5.5
class EF_MSISDN(LinFixedEF):
def __init__(self, fid='6f40', sfid=None, name='EF.MSISDN', desc='MSISDN', **kwargs):
- super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(15, 34), **kwargs)
+ super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(15, 34), leftpad=True, **kwargs)
def _decode_record_hex(self, raw_hex_data, **kwargs):
return {'msisdn': dec_msisdn(raw_hex_data)}
@@ -421,8 +198,7 @@ class EF_MSISDN(LinFixedEF):
encoded_msisdn = enc_msisdn(msisdn)
else:
encoded_msisdn = enc_msisdn(msisdn[2], msisdn[0], msisdn[1])
- alpha_identifier = (list(self.rec_len)[
- 0] - len(encoded_msisdn) // 2) * "ff"
+ alpha_identifier = (list(self.rec_len)[0] - len(encoded_msisdn) // 2) * "ff"
return alpha_identifier + encoded_msisdn
# TS 51.011 Section 10.5.6
@@ -440,6 +216,7 @@ class EF_SMSP(LinFixedEF):
"call_number": "" },
"tp_pid": "00", "tp_dcs": "00", "tp_vp_minutes": 1440 } ),
]
+ _test_no_pad = True
class ValidityPeriodAdapter(Adapter):
def _decode(self, obj, context, path):
if obj <= 143:
@@ -739,7 +516,7 @@ class EF_SPN(TransparentEF):
'hide_in_oplmn'/Flag,
'show_in_hplmn'/Flag,
# Bytes 2..17
- 'spn'/Bytewise(GsmString(16))
+ 'spn'/Bytewise(GsmOrUcs2String(16))
)
# TS 51.011 Section 10.3.13
@@ -790,6 +567,8 @@ class EF_AD(TransparentEF):
( "00ffff",
{ "ms_operation_mode": "normal", "rfu1": 255, "rfu2": 127, "ofm": True, "extensions": None } ),
]
+ _test_no_pad = True
+
class OP_MODE(enum.IntEnum):
normal = 0x00
type_approval = 0x80
@@ -958,8 +737,9 @@ class EF_LOCIGPRS(TransparentEF):
# TS 51.011 Section 10.3.35..37
class EF_xPLMNwAcT(TransRecEF):
_test_de_encode = [
- ( '62F2104000', { "mcc": "262", "mnc": "01", "act": [ "E-UTRAN" ] } ),
+ ( '62F2104000', { "mcc": "262", "mnc": "01", "act": [ "E-UTRAN NB-S1", "E-UTRAN WB-S1" ] } ),
( '62F2108000', { "mcc": "262", "mnc": "01", "act": [ "UTRAN" ] } ),
+ ( '62F220488C', { "mcc": "262", "mnc": "02", "act": ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'NG-RAN'] } ),
]
def __init__(self, fid='1234', sfid=None, name=None, desc=None, size=(40, None), rec_len=5, **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
@@ -993,18 +773,18 @@ class EF_xPLMNwAcT(TransRecEF):
if 'cdma2000 1xRTT' in in_list:
u16 |= 0x0010
# E-UTRAN
- if 'E-UTRAN' in in_list:
+ if 'E-UTRAN WB-S1' in in_list and 'E-UTRAN NB-S1' in in_list:
u16 |= 0x4000
- if 'E-UTRAN WB-S1' in in_list:
+ elif 'E-UTRAN WB-S1' in in_list:
u16 |= 0x6000
- if 'E-UTRAN NB-S1' in in_list:
+ elif 'E-UTRAN NB-S1' in in_list:
u16 |= 0x5000
# GSM mess
if 'GSM' in in_list and 'EC-GSM-IoT' in in_list:
u16 |= 0x008C
elif 'GSM' in in_list:
u16 |= 0x0084
- elif 'EC-GSM-IuT' in in_list:
+ elif 'EC-GSM-IoT' in in_list:
u16 |= 0x0088
return '%04X' % (u16)
@@ -1023,6 +803,27 @@ class EF_InvScan(TransparentEF):
self._construct = FlagsEnum(
Byte, in_limited_service_mode=1, after_successful_plmn_selection=2)
+# TS 51.011 Section 10.3.46
+class EF_CFIS(LinFixedEF):
+ _test_decode = [
+ ( '0100ffffffffffffffffffffffffffff',
+ {"msp_number": 1, "cfu_indicator_status": { "voice": False, "fax": False, "data": False, "rfu": 0 },
+ "len_of_bcd": 255, "ton_npi": {"ext": True,
+ "type_of_number": "reserved_for_extension",
+ "numbering_plan_id": "reserved_for_extension"},
+ "dialing_nr": "", "cap_conf_id": 255, "ext7_record_id": 255} ),
+ ]
+ def __init__(self, fid='6fcb', sfid=None, name='EF.CFIS', desc='Call Forwarding Indication Status', ext=7, **kwargs):
+ super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(16, 30), **kwargs)
+ ext_name = 'ext%u_record_id' % ext
+ self._construct = Struct('msp_number'/Int8ub,
+ 'cfu_indicator_status'/BitStruct('voice'/Flag, 'fax'/Flag, 'data'/Flag, 'rfu'/BitsRFU(5)),
+ 'len_of_bcd'/Int8ub,
+ 'ton_npi'/TonNpi,
+ 'dialing_nr'/ExtendedBcdAdapter(BcdAdapter(Rpad(Bytes(10)))),
+ 'cap_conf_id'/Int8ub,
+ ext_name/Int8ub)
+
# TS 51.011 Section 4.2.58
class EF_PNN(LinFixedEF):
# TODO: 430a82d432bbbc7eb75de432450a82d432bbbc7eb75de432ffffffff
@@ -1048,11 +849,11 @@ class EF_PNN(LinFixedEF):
class EF_OPL(LinFixedEF):
_test_de_encode = [
( '62f2100000fffe01',
- { "lai": { "mcc_mnc": "262f01", "lac_min": "0000", "lac_max": "fffe" }, "pnn_record_id": 1 } ),
+ { "lai": { "mcc_mnc": "262-01", "lac_min": "0000", "lac_max": "fffe" }, "pnn_record_id": 1 } ),
]
def __init__(self, fid='6fc6', sfid=None, name='EF.OPL', rec_len=(8, 8), desc='Operator PLMN List', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
- self._construct = Struct('lai'/Struct('mcc_mnc'/BcdAdapter(Bytes(3)),
+ self._construct = Struct('lai'/Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
'lac_min'/HexAdapter(Bytes(2)), 'lac_max'/HexAdapter(Bytes(2))), 'pnn_record_id'/Int8ub)
# TS 51.011 Section 10.3.44 + TS 31.102 4.2.62
@@ -1068,6 +869,14 @@ class EF_MBI(LinFixedEF):
# TS 51.011 Section 10.3.45 + TS 31.102 4.2.63
class EF_MWIS(LinFixedEF):
+ _test_de_encode = [
+ ( '0000000000',
+ {"mwi_status": {"voicemail": False, "fax": False, "email": False, "other": False, "videomail":
+ False}, "num_waiting_voicemail": 0, "num_waiting_fax": 0, "num_waiting_email": 0,
+ "num_waiting_other": 0, "num_waiting_videomail": None} ),
+ ]
+ _test_no_pad = True
+
def __init__(self, fid='6fca', sfid=None, name='EF.MWIS', rec_len=(5, 6),
desc='Message Waiting Indication Status', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
@@ -1081,7 +890,7 @@ class EF_SPDI(TransparentEF):
# TODO: a305800337f800ffffffffffffffffffffffffffffffffffffffffffffff
class ServiceProviderPLMN(BER_TLV_IE, tag=0x80):
# flexible numbers of 3-byte PLMN records
- _construct = GreedyRange(BcdAdapter(Bytes(3)))
+ _construct = GreedyRange(PlmnAdapter(Bytes(3)))
class SPDI(BER_TLV_IE, tag=0xA3, nested=[ServiceProviderPLMN]):
pass
@@ -1126,7 +935,7 @@ class EF_MMSICP(TransparentEF):
# TS 51.011 Section 10.3.54
class EF_MMSUP(LinFixedEF):
class MMS_UserPref_ProfileName(BER_TLV_IE, tag=0x81):
- pass
+ _construct = GsmOrUcs2Adapter(GreedyBytes)
class MMS_UserPref_Info(BER_TLV_IE, tag=0x82):
pass
@@ -1157,32 +966,32 @@ class DF_GSM(CardDF):
EF_Kc(),
EF_PLMNsel(),
TransparentEF('6f31', None, 'EF.HPPLMN',
- 'Higher Priority PLMN search period'),
+ desc='Higher Priority PLMN search period'),
EF_ACMmax(),
EF_ServiceTable('6f38', None, 'EF.SST',
- 'SIM service table', table=EF_SST_map, size=(2, 16)),
+ desc='SIM service table', table=EF_SST_map, size=(2, 16)),
CyclicEF('6f39', None, 'EF.ACM',
- 'Accumulated call meter', rec_len=(3, 3)),
- TransparentEF('6f3e', None, 'EF.GID1', 'Group Identifier Level 1'),
- TransparentEF('6f3f', None, 'EF.GID2', 'Group Identifier Level 2'),
+ desc='Accumulated call meter', rec_len=(3, 3)),
+ TransparentEF('6f3e', None, 'EF.GID1', desc='Group Identifier Level 1'),
+ TransparentEF('6f3f', None, 'EF.GID2', desc='Group Identifier Level 2'),
EF_SPN(),
TransparentEF('6f41', None, 'EF.PUCT',
- 'Price per unit and currency table', size=(5, 5)),
+ desc='Price per unit and currency table', size=(5, 5)),
EF_CBMI(),
TransparentEF('6f74', None, 'EF.BCCH',
- 'Broadcast control channels', size=(16, 16)),
+ desc='Broadcast control channels', size=(16, 16)),
EF_ACC(),
EF_PLMNsel('6f7b', None, 'EF.FPLMN',
- 'Forbidden PLMNs', size=(12, 12)),
+ desc='Forbidden PLMNs', size=(12, 12)),
EF_LOCI(),
EF_AD(),
TransparentEF('6fae', None, 'EF.Phase',
- 'Phase identification', size=(1, 1)),
+ desc='Phase identification', size=(1, 1)),
EF_VGCS(),
EF_VGCSS(),
- EF_VGCS('6fb3', None, 'EF.VBS', 'Voice Broadcast Service'),
+ EF_VGCS('6fb3', None, 'EF.VBS', desc='Voice Broadcast Service'),
EF_VGCSS('6fb4', None, 'EF.VBSS',
- 'Voice Broadcast Service Status'),
+ desc='Voice Broadcast Service Status'),
EF_eMLPP(),
EF_AAeM(),
EF_CBMID(),
@@ -1191,29 +1000,28 @@ class DF_GSM(CardDF):
EF_DCK(),
EF_CNL(),
EF_NIA(),
- EF_Kc('6f52', None, 'EF.KcGPRS', 'GPRS Ciphering key KcGPRS'),
+ EF_Kc('6f52', None, 'EF.KcGPRS', desc='GPRS Ciphering key KcGPRS'),
EF_LOCIGPRS(),
- TransparentEF('6f54', None, 'EF.SUME', 'SetUpMenu Elements'),
+ TransparentEF('6f54', None, 'EF.SUME', desc='SetUpMenu Elements'),
EF_xPLMNwAcT('6f60', None, 'EF.PLMNwAcT',
- 'User controlled PLMN Selector with Access Technology'),
+ desc='User controlled PLMN Selector with Access Technology'),
EF_xPLMNwAcT('6f61', None, 'EF.OPLMNwAcT',
- 'Operator controlled PLMN Selector with Access Technology'),
+ desc='Operator controlled PLMN Selector with Access Technology'),
EF_xPLMNwAcT('6f62', None, 'EF.HPLMNwAcT',
- 'HPLMN Selector with Access Technology'),
+ desc='HPLMN Selector with Access Technology'),
EF_CPBCCH(),
EF_InvScan(),
EF_PNN(),
EF_OPL(),
- EF_ADN('6fc7', None, 'EF.MBDN', 'Mailbox Dialling Numbers'),
+ EF_ADN('6fc7', None, 'EF.MBDN', desc='Mailbox Dialling Numbers'),
EF_MBI(),
EF_MWIS(),
- EF_ADN('6fcb', None, 'EF.CFIS',
- 'Call Forwarding Indication Status'),
- EF_EXT('6fc8', None, 'EF.EXT6', 'Externsion6 (MBDN)'),
- EF_EXT('6fcc', None, 'EF.EXT7', 'Externsion7 (CFIS)'),
+ EF_CFIS(),
+ EF_EXT('6fc8', None, 'EF.EXT6', desc='Externsion6 (MBDN)'),
+ EF_EXT('6fcc', None, 'EF.EXT7', desc='Externsion7 (CFIS)'),
EF_SPDI(),
EF_MMSN(),
- EF_EXT('6fcf', None, 'EF.EXT8', 'Extension8 (MMSN)'),
+ EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)'),
EF_MMSICP(),
EF_MMSUP(),
EF_MMSUCP(),
@@ -1226,12 +1034,12 @@ class DF_GSM(CardDF):
super().__init__()
authenticate_parser = argparse.ArgumentParser()
- authenticate_parser.add_argument('rand', help='Random challenge')
+ authenticate_parser.add_argument('rand', type=is_hexstr, help='Random challenge')
@cmd2.with_argparser(authenticate_parser)
def do_authenticate(self, opts):
"""Perform GSM Authentication."""
- (data, sw) = self._cmd.card._scc.run_gsm(opts.rand)
+ (data, sw) = self._cmd.lchan.scc.run_gsm(opts.rand)
self._cmd.poutput_json(data)
@@ -1277,8 +1085,12 @@ class CardProfileSIM(CardProfile):
},
}
+ addons = [
+ AddonGSMR,
+ ]
+
super().__init__('SIM', desc='GSM SIM Card', cla="a0",
- sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM()], sw=sw)
+ sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM()], sw=sw, addons = addons)
@staticmethod
def decode_select_response(resp_hex: str) -> object:
@@ -1334,3 +1146,17 @@ class CardProfileSIM(CardProfile):
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_sim(scc)
+
+
+class AddonSIM(CardProfileAddon):
+ """An add-on that can be found on a UICC in order to support classic GSM SIM."""
+ def __init__(self):
+ files = [
+ DF_GSM(),
+ DF_TELECOM(),
+ ]
+ super().__init__('SIM', desc='GSM SIM', files_in_mf=files)
+
+ def probe(self, card:'CardBase') -> bool:
+ # we assume the add-on to be present in case DF.GSM is found on the card
+ return card.file_exists(self.files_in_mf[0].fid)
diff --git a/pySim/utils.py b/pySim/utils.py
index c6362fa..2362b59 100644
--- a/pySim/utils.py
+++ b/pySim/utils.py
@@ -6,8 +6,10 @@
import json
import abc
import string
+import datetime
+import argparse
from io import BytesIO
-from typing import Optional, List, Dict, Any, Tuple
+from typing import Optional, List, Dict, Any, Tuple, NewType, Union
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
@@ -27,8 +29,10 @@ from typing import Optional, List, Dict, Any, Tuple
#
# just to differentiate strings of hex nibbles from everything else
-Hexstr = str
-
+Hexstr = NewType('Hexstr', str)
+SwHexstr = NewType('SwHexstr', str)
+SwMatchstr = NewType('SwMatchstr', str)
+ResTuple = Tuple[Hexstr, SwHexstr]
def h2b(s: Hexstr) -> bytearray:
"""convert from a string of hex nibbles to a sequence of bytes"""
@@ -146,12 +150,12 @@ def comprehensiontlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
# three-byte tag
tag = (binary[1] & 0x7f) << 8
tag |= binary[2]
- compr = True if binary[1] & 0x80 else False
+ compr = bool(binary[1] & 0x80)
return ({'comprehension': compr, 'tag': tag}, binary[3:])
else:
# single byte tag
tag = binary[0] & 0x7f
- compr = True if binary[0] & 0x80 else False
+ compr = bool(binary[0] & 0x80)
return ({'comprehension': compr, 'tag': tag}, binary[1:])
@@ -159,7 +163,7 @@ def comprehensiontlv_encode_tag(tag) -> bytes:
"""Encode a single Tag according to ETSI TS 101 220 Section 7.1.1"""
# permit caller to specify tag also as integer value
if isinstance(tag, int):
- compr = True if tag < 0xff and tag & 0x80 else False
+ compr = bool(tag < 0xff and tag & 0x80)
tag = {'tag': tag, 'comprehension': compr}
compr = tag.get('comprehension', False)
if tag['tag'] in [0x00, 0x80, 0xff] or tag['tag'] > 0xff:
@@ -215,7 +219,7 @@ def bertlv_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
i = 1
last = False
while not last:
- last = False if binary[i] & 0x80 else True
+ last = not bool(binary[i] & 0x80)
tag <<= 8
tag |= binary[i]
i += 1
@@ -230,7 +234,7 @@ def bertlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
Tuple of ({class:int, constructed:bool, tag:int}, remainder:bytes)
"""
cls = binary[0] >> 6
- constructed = True if binary[0] & 0x20 else False
+ constructed = bool(binary[0] & 0x20)
tag = binary[0] & 0x1f
if tag <= 30:
return ({'class': cls, 'constructed': constructed, 'tag': tag}, binary[1:])
@@ -239,7 +243,7 @@ def bertlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
i = 1
last = False
while not last:
- last = False if binary[i] & 0x80 else True
+ last = not bool(binary[i] & 0x80)
tag <<= 7
tag |= binary[i] & 0x7f
i += 1
@@ -260,15 +264,22 @@ def bertlv_encode_tag(t) -> bytes:
remainder = inp & ~ (inp << (remain_bits - bitcnt))
return outp, remainder
+ def count_int_bytes(inp: int) -> int:
+ """count the number of bytes require to represent the given integer."""
+ i = 1
+ inp = inp >> 8
+ while inp:
+ i += 1
+ inp = inp >> 8
+ return i
+
if isinstance(t, int):
- # FIXME: multiple byte tags
- tag = t & 0x1f
- constructed = True if t & 0x20 else False
- cls = t >> 6
- else:
- tag = t['tag']
- constructed = t['constructed']
- cls = t['class']
+ # first convert to a dict representation
+ tag_size = count_int_bytes(t)
+ t, _remainder = bertlv_parse_tag(t.to_bytes(tag_size, 'big'))
+ tag = t['tag']
+ constructed = t['constructed']
+ cls = t['class']
if tag <= 30:
t = tag & 0x1f
if constructed:
@@ -305,6 +316,8 @@ def bertlv_parse_len(binary: bytes) -> Tuple[int, bytes]:
else:
num_len_oct = binary[0] & 0x7f
length = 0
+ if len(binary) < num_len_oct + 1:
+ return (0, b'')
for i in range(1, 1+num_len_oct):
length <<= 8
length |= binary[i]
@@ -346,6 +359,68 @@ def bertlv_parse_one(binary: bytes) -> Tuple[dict, int, bytes, bytes]:
remainder = remainder[length:]
return (tagdict, length, value, remainder)
+def bertlv_parse_one_rawtag(binary: bytes) -> Tuple[int, int, bytes, bytes]:
+ """Parse a single TLV IE at the start of the given binary data; return tag as raw integer.
+ Args:
+ binary : binary input data of BER-TLV length field
+ Returns:
+ Tuple of (tag:int, len:int, remainder:bytes)
+ """
+ (tag, remainder) = bertlv_parse_tag_raw(binary)
+ (length, remainder) = bertlv_parse_len(remainder)
+ value = remainder[:length]
+ remainder = remainder[length:]
+ return (tag, length, value, remainder)
+
+def bertlv_return_one_rawtlv(binary: bytes) -> Tuple[int, int, bytes, bytes]:
+ """Return one single [encoded] TLV IE at the start of the given binary data.
+ Args:
+ binary : binary input data of BER-TLV length field
+ Returns:
+ Tuple of (tag:int, len:int, tlv:bytes, remainder:bytes)
+ """
+ (tag, remainder) = bertlv_parse_tag_raw(binary)
+ (length, remainder) = bertlv_parse_len(remainder)
+ tl_length = len(binary) - len(remainder)
+ value = binary[:tl_length] + remainder[:length]
+ remainder = remainder[length:]
+ return (tag, length, value, remainder)
+
+def dgi_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
+ # In absence of any clear spec guidance we assume it's always 16 bit
+ return int.from_bytes(binary[:2], 'big'), binary[2:]
+
+def dgi_encode_tag(t: int) -> bytes:
+ return t.to_bytes(2, 'big')
+
+def dgi_encode_len(length: int) -> bytes:
+ """Encode a single Length value according to GlobalPlatform Systems Scripting Language
+ Specification v1.1.0 Annex B.
+ Args:
+ length : length value to be encoded
+ Returns:
+ binary output data of encoded length field
+ """
+ if length < 255:
+ return length.to_bytes(1, 'big')
+ elif length <= 0xffff:
+ return b'\xff' + length.to_bytes(2, 'big')
+ else:
+ raise ValueError("Length > 32bits not supported")
+
+def dgi_parse_len(binary: bytes) -> Tuple[int, bytes]:
+ """Parse a single Length value according to GlobalPlatform Systems Scripting Language
+ Specification v1.1.0 Annex B.
+ Args:
+ binary : binary input data of BER-TLV length field
+ Returns:
+ Tuple of (length, remainder)
+ """
+ if binary[0] == 255:
+ assert len(binary) >= 3
+ return ((binary[1] << 8) | binary[2]), binary[3:]
+ else:
+ return binary[0], binary[1:]
# IMSI encoded format:
# For IMSI 0123456789ABCDE:
@@ -397,6 +472,30 @@ def dec_iccid(ef: Hexstr) -> str:
def enc_iccid(iccid: str) -> Hexstr:
return swap_nibbles(rpad(iccid, 20))
+def sanitize_iccid(iccid: Union[int, str]) -> str:
+ iccid = str(iccid)
+ if len(iccid) < 18:
+ raise ValueError('ICCID input value must be at least 18 digits')
+ if len(iccid) > 20:
+ raise ValueError('ICCID input value must be at most 20 digits')
+ if len(iccid) == 18:
+ # 18 digits means we must add a luhn check digit to reach 19 digits
+ iccid += str(calculate_luhn(iccid))
+ if len(iccid) == 20:
+ # 20 digits means we're actually exceeding E.118 by one digit, and
+ # the luhn check digit must already be included
+ verify_luhn(iccid)
+ if len(iccid) == 19:
+ # 19 digits means that it's either an in-spec 19-digits ICCID with
+ # its luhn check digit already present, or it's an out-of-spec 20-digit
+ # ICCID without that check digit...
+ try:
+ verify_luhn(iccid)
+ except ValueError:
+ # 19th digit was not luhn check digit; we must add it
+ iccid += str(calculate_luhn(iccid))
+ return iccid
+
def enc_plmn(mcc: Hexstr, mnc: Hexstr) -> Hexstr:
"""Converts integer MCC/MNC into 3 bytes for EF"""
@@ -411,7 +510,7 @@ def enc_plmn(mcc: Hexstr, mnc: Hexstr) -> Hexstr:
if len(mnc) == 0:
mnc = "FFF"
elif len(mnc) == 1:
- mnc = "F0" + mnc
+ mnc = "0" + mnc + "F"
elif len(mnc) == 2:
mnc += "F"
@@ -433,30 +532,6 @@ def dec_plmn(threehexbytes: Hexstr) -> dict:
return res
-def dec_spn(ef):
- """Obsolete, kept for API compatibility"""
- from ts_51_011 import EF_SPN
- abstract_data = EF_SPN().decode_hex(ef)
- show_in_hplmn = abstract_data['show_in_hplmn']
- hide_in_oplmn = abstract_data['hide_in_oplmn']
- name = abstract_data['spn']
- return (name, show_in_hplmn, hide_in_oplmn)
-
-
-def enc_spn(name: str, show_in_hplmn=False, hide_in_oplmn=False):
- """Obsolete, kept for API compatibility"""
- from ts_51_011 import EF_SPN
- abstract_data = {
- 'hide_in_oplmn': hide_in_oplmn,
- 'show_in_hplmn': show_in_hplmn,
- 'spn': name,
- }
- return EF_SPN().encode_hex(abstract_data)
-
-
-def hexstr_to_Nbytearr(s, nbytes):
- return [s[i:i+(nbytes*2)] for i in range(0, len(s), (nbytes*2))]
-
# Accepts hex string representing three bytes
@@ -499,33 +574,37 @@ def dec_mnc_from_plmn_str(plmn: Hexstr) -> str:
def dec_act(twohexbytes: Hexstr) -> List[str]:
act_list = [
{'bit': 15, 'name': "UTRAN"},
- {'bit': 14, 'name': "E-UTRAN"},
{'bit': 11, 'name': "NG-RAN"},
- {'bit': 7, 'name': "GSM"},
{'bit': 6, 'name': "GSM COMPACT"},
{'bit': 5, 'name': "cdma2000 HRPD"},
{'bit': 4, 'name': "cdma2000 1xRTT"},
]
ia = h2i(twohexbytes)
u16t = (ia[0] << 8) | ia[1]
- sel = []
+ sel = set()
+ # only the simple single-bit ones
for a in act_list:
if u16t & (1 << a['bit']):
- if a['name'] == "E-UTRAN":
- # The Access technology identifier of E-UTRAN
- # allows a more detailed specification:
- if u16t & (1 << 13) and u16t & (1 << 12):
- sel.append("E-UTRAN WB-S1")
- sel.append("E-UTRAN NB-S1")
- elif u16t & (1 << 13):
- sel.append("E-UTRAN WB-S1")
- elif u16t & (1 << 12):
- sel.append("E-UTRAN NB-S1")
- else:
- sel.append("E-UTRAN")
- else:
- sel.append(a['name'])
- return sel
+ sel.add(a['name'])
+ # TS 31.102 Section 4.2.5 Table 4.2.5.1
+ eutran_bits = u16t & 0x7000
+ if eutran_bits in [0x4000, 0x7000]:
+ sel.add("E-UTRAN WB-S1")
+ sel.add("E-UTRAN NB-S1")
+ elif eutran_bits == 0x5000:
+ sel.add("E-UTRAN NB-S1")
+ elif eutran_bits == 0x6000:
+ sel.add("E-UTRAN WB-S1")
+ # TS 31.102 Section 4.2.5 Table 4.2.5.2
+ gsm_bits = u16t & 0x008C
+ if gsm_bits in [0x0080, 0x008C]:
+ sel.add("GSM")
+ sel.add("EC-GSM-IoT")
+ elif u16t & 0x008C == 0x0084:
+ sel.add("GSM")
+ elif u16t & 0x008C == 0x0086:
+ sel.add("EC-GSM-IoT")
+ return sorted(list(sel))
def dec_xplmn_w_act(fivehexbytes: Hexstr) -> Dict[str, Any]:
@@ -542,53 +621,6 @@ def dec_xplmn_w_act(fivehexbytes: Hexstr) -> Dict[str, Any]:
return res
-def format_xplmn_w_act(hexstr):
- s = ""
- for rec_data in hexstr_to_Nbytearr(hexstr, 5):
- rec_info = dec_xplmn_w_act(rec_data)
- if rec_info['mcc'] == "" and rec_info['mnc'] == "":
- rec_str = "unused"
- else:
- rec_str = "MCC: %s MNC: %s AcT: %s" % (
- rec_info['mcc'], rec_info['mnc'], ", ".join(rec_info['act']))
- s += "\t%s # %s\n" % (rec_data, rec_str)
- return s
-
-
-def dec_loci(hexstr):
- res = {'tmsi': '', 'mcc': 0, 'mnc': 0, 'lac': '', 'status': 0}
- res['tmsi'] = hexstr[:8]
- res['mcc'] = dec_mcc_from_plmn(hexstr[8:14])
- res['mnc'] = dec_mnc_from_plmn(hexstr[8:14])
- res['lac'] = hexstr[14:18]
- res['status'] = h2i(hexstr[20:22])
- return res
-
-
-def dec_psloci(hexstr):
- res = {'p-tmsi': '', 'p-tmsi-sig': '', 'mcc': 0,
- 'mnc': 0, 'lac': '', 'rac': '', 'status': 0}
- res['p-tmsi'] = hexstr[:8]
- res['p-tmsi-sig'] = hexstr[8:14]
- res['mcc'] = dec_mcc_from_plmn(hexstr[14:20])
- res['mnc'] = dec_mnc_from_plmn(hexstr[14:20])
- res['lac'] = hexstr[20:24]
- res['rac'] = hexstr[24:26]
- res['status'] = h2i(hexstr[26:28])
- return res
-
-
-def dec_epsloci(hexstr):
- res = {'guti': '', 'mcc': 0, 'mnc': 0, 'tac': '', 'status': 0}
- res['guti'] = hexstr[:24]
- res['tai'] = hexstr[24:34]
- res['mcc'] = dec_mcc_from_plmn(hexstr[24:30])
- res['mnc'] = dec_mnc_from_plmn(hexstr[24:30])
- res['tac'] = hexstr[30:34]
- res['status'] = h2i(hexstr[34:36])
- return res
-
-
def dec_xplmn(threehexbytes: Hexstr) -> dict:
res = {'mcc': 0, 'mnc': 0, 'act': []}
plmn_chars = 6
@@ -599,18 +631,6 @@ def dec_xplmn(threehexbytes: Hexstr) -> dict:
return res
-def format_xplmn(hexstr: Hexstr) -> str:
- s = ""
- for rec_data in hexstr_to_Nbytearr(hexstr, 3):
- rec_info = dec_xplmn(rec_data)
- if not rec_info['mcc'] and not rec_info['mnc']:
- rec_str = "unused"
- else:
- rec_str = "MCC: %s MNC: %s" % (rec_info['mcc'], rec_info['mnc'])
- s += "\t%s # %s\n" % (rec_data, rec_str)
- return s
-
-
def derive_milenage_opc(ki_hex: Hexstr, op_hex: Hexstr) -> Hexstr:
"""
Run the milenage algorithm to calculate OPC from Ki and OP
@@ -618,7 +638,6 @@ def derive_milenage_opc(ki_hex: Hexstr, op_hex: Hexstr) -> Hexstr:
from Cryptodome.Cipher import AES
# pylint: disable=no-name-in-module
from Cryptodome.Util.strxor import strxor
- from pySim.utils import b2h
# We pass in hex string and now need to work on bytes
ki_bytes = bytes(h2b(ki_hex))
@@ -637,12 +656,17 @@ def calculate_luhn(cc) -> int:
for d in num[::-2]]) % 10
return 0 if check_digit == 10 else check_digit
+def verify_luhn(digits: str):
+ """Verify the Luhn check digit; raises ValueError if it is incorrect."""
+ cd = calculate_luhn(digits[:-1])
+ if str(cd) != digits[-1]:
+ raise ValueError('Luhn check digit mismatch: should be %s but is %s' % (str(cd), digits[-1]))
def mcc_from_imsi(imsi: str) -> Optional[str]:
"""
Derive the MCC (Mobile Country Code) from the first three digits of an IMSI
"""
- if imsi == None:
+ if imsi is None:
return None
if len(imsi) > 3:
@@ -655,7 +679,7 @@ def mnc_from_imsi(imsi: str, long: bool = False) -> Optional[str]:
"""
Derive the MNC (Mobile Country Code) from the 4th to 6th digit of an IMSI
"""
- if imsi == None:
+ if imsi is None:
return None
if len(imsi) > 3:
@@ -761,7 +785,7 @@ def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
"""
# If no MSISDN is supplied then encode the file contents as all "ff"
- if msisdn == "" or msisdn == "+":
+ if msisdn in ["", "+"]:
return "ff" * 14
# Leading '+' indicates International Number
@@ -785,202 +809,6 @@ def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
return ('%02x' % bcd_len) + ('%02x' % npi_ton) + bcd + ("ff" * 2)
-def dec_st(st, table="sim") -> str:
- """
- Parses the EF S/U/IST and prints the list of available services in EF S/U/IST
- """
-
- if table == "isim":
- from pySim.ts_31_103 import EF_IST_map
- lookup_map = EF_IST_map
- elif table == "usim":
- from pySim.ts_31_102 import EF_UST_map
- lookup_map = EF_UST_map
- else:
- from pySim.ts_51_011 import EF_SST_map
- lookup_map = EF_SST_map
-
- st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
-
- avail_st = ""
- # Get each byte and check for available services
- for i in range(0, len(st_bytes)):
- # Byte i contains info about Services num (8i+1) to num (8i+8)
- byte = int(st_bytes[i], 16)
- # Services in each byte are in order MSB to LSB
- # MSB - Service (8i+8)
- # LSB - Service (8i+1)
- for j in range(1, 9):
- if byte & 0x01 == 0x01 and ((8*i) + j in lookup_map):
- # Byte X contains info about Services num (8X-7) to num (8X)
- # bit = 1: service available
- # bit = 0: service not available
- avail_st += '\tService %d - %s\n' % (
- (8*i) + j, lookup_map[(8*i) + j])
- byte = byte >> 1
- return avail_st
-
-
-def first_TLV_parser(bytelist):
- '''
- first_TLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) -> (170, 2, [171, 205])
-
- parses first TLV format record in a list of bytelist
- returns a 3-Tuple: Tag, Length, Value
- Value is a list of bytes
- parsing of length is ETSI'style 101.220
- '''
- Tag = bytelist[0]
- if bytelist[1] == 0xFF:
- Len = bytelist[2]*256 + bytelist[3]
- Val = bytelist[4:4+Len]
- else:
- Len = bytelist[1]
- Val = bytelist[2:2+Len]
- return (Tag, Len, Val)
-
-
-def TLV_parser(bytelist):
- '''
- TLV_parser([0xAA, ..., 0xFF]) -> [(T, L, [V]), (T, L, [V]), ...]
-
- loops on the input list of bytes with the "first_TLV_parser()" function
- returns a list of 3-Tuples
- '''
- ret = []
- while len(bytelist) > 0:
- T, L, V = first_TLV_parser(bytelist)
- if T == 0xFF:
- # padding bytes
- break
- ret.append((T, L, V))
- # need to manage length of L
- if L > 0xFE:
- bytelist = bytelist[L+4:]
- else:
- bytelist = bytelist[L+2:]
- return ret
-
-
-def enc_st(st, service, state=1):
- """
- Encodes the EF S/U/IST/EST and returns the updated Service Table
-
- Parameters:
- st - Current value of SIM/USIM/ISIM Service Table
- service - Service Number to encode as activated/de-activated
- state - 1 mean activate, 0 means de-activate
-
- Returns:
- s - Modified value of SIM/USIM/ISIM Service Table
-
- Default values:
- - state: 1 - Sets the particular Service bit to 1
- """
- st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
-
- s = ""
- # Check whether the requested service is present in each byte
- for i in range(0, len(st_bytes)):
- # Byte i contains info about Services num (8i+1) to num (8i+8)
- if service in range((8*i) + 1, (8*i) + 9):
- byte = int(st_bytes[i], 16)
- # Services in each byte are in order MSB to LSB
- # MSB - Service (8i+8)
- # LSB - Service (8i+1)
- mod_byte = 0x00
- # Copy bit by bit contents of byte to mod_byte with modified bit
- # for requested service
- for j in range(1, 9):
- mod_byte = mod_byte >> 1
- if service == (8*i) + j:
- mod_byte = state == 1 and mod_byte | 0x80 or mod_byte & 0x7f
- else:
- mod_byte = byte & 0x01 == 0x01 and mod_byte | 0x80 or mod_byte & 0x7f
- byte = byte >> 1
-
- s += ('%02x' % (mod_byte))
- else:
- s += st_bytes[i]
-
- return s
-
-
-def dec_addr_tlv(hexstr):
- """
- Decode hex string to get EF.P-CSCF Address or EF.ePDGId or EF.ePDGIdEm.
- See 3GPP TS 31.102 version 13.4.0 Release 13, section 4.2.8, 4.2.102 and 4.2.104.
- """
-
- # Convert from hex str to int bytes list
- addr_tlv_bytes = h2i(hexstr)
-
- # Get list of tuples containing parsed TLVs
- tlvs = TLV_parser(addr_tlv_bytes)
-
- for tlv in tlvs:
- # tlv = (T, L, [V])
- # T = Tag
- # L = Length
- # [V] = List of value
-
- # Invalid Tag value scenario
- if tlv[0] != 0x80:
- continue
-
- # Empty field - Zero length
- if tlv[1] == 0:
- continue
-
- # First byte in the value has the address type
- addr_type = tlv[2][0]
- # TODO: Support parsing of IPv6
- # Address Type: 0x00 (FQDN), 0x01 (IPv4), 0x02 (IPv6), other (Reserved)
- if addr_type == 0x00: # FQDN
- # Skip address tye byte i.e. first byte in value list
- content = tlv[2][1:]
- return (i2s(content), '00')
-
- elif addr_type == 0x01: # IPv4
- # Skip address tye byte i.e. first byte in value list
- # Skip the unused byte in Octect 4 after address type byte as per 3GPP TS 31.102
- ipv4 = tlv[2][2:]
- content = '.'.join(str(x) for x in ipv4)
- return (content, '01')
- else:
- raise ValueError("Invalid address type")
-
- return (None, None)
-
-
-def enc_addr_tlv(addr, addr_type='00'):
- """
- Encode address TLV object used in EF.P-CSCF Address, EF.ePDGId and EF.ePDGIdEm.
- See 3GPP TS 31.102 version 13.4.0 Release 13, section 4.2.8, 4.2.102 and 4.2.104.
-
- Default values:
- - addr_type: 00 - FQDN format of Address
- """
-
- s = ""
-
- # TODO: Encoding of IPv6 address
- if addr_type == '00': # FQDN
- hex_str = s2h(addr)
- s += '80' + ('%02x' % ((len(hex_str)//2)+1)) + '00' + hex_str
- elif addr_type == '01': # IPv4
- ipv4_list = addr.split('.')
- ipv4_str = ""
- for i in ipv4_list:
- ipv4_str += ('%02x' % (int(i)))
-
- # Unused bytes shall be set to 'ff'. i.e 4th Octet after Address Type is not used
- # IPv4 Address is in octet 5 to octet 8 of the TLV data object
- s += '80' + ('%02x' % ((len(ipv4_str)//2)+2)) + '01' + 'ff' + ipv4_str
-
- return s
-
-
def is_hex(string: str, minlen: int = 2, maxlen: Optional[int] = None) -> bool:
"""
Check if a string is a valid hexstring
@@ -998,7 +826,7 @@ def is_hex(string: str, minlen: int = 2, maxlen: Optional[int] = None) -> bool:
# Try actual encoding to be sure
try:
- try_encode = h2b(string)
+ _try_encode = h2b(string)
return True
except:
return False
@@ -1026,98 +854,14 @@ def sanitize_pin_adm(pin_adm, pin_adm_hex=None) -> Hexstr:
# Ensure that it's hex-encoded
try:
try_encode = h2b(pin_adm)
- except ValueError:
- raise ValueError(
- "PIN-ADM needs to be hex encoded using this option")
+ except ValueError as exc:
+ raise ValueError("PIN-ADM needs to be hex encoded using this option") from exc
else:
- raise ValueError(
- "PIN-ADM needs to be exactly 16 digits (hex encoded)")
+ raise ValueError("PIN-ADM needs to be exactly 16 digits (hex encoded)")
return pin_adm
-def enc_ePDGSelection(hexstr, mcc, mnc, epdg_priority='0001', epdg_fqdn_format='00'):
- """
- Encode ePDGSelection so it can be stored at EF.ePDGSelection or EF.ePDGSelectionEm.
- See 3GPP TS 31.102 version 15.2.0 Release 15, section 4.2.104 and 4.2.106.
-
- Default values:
- - epdg_priority: '0001' - 1st Priority
- - epdg_fqdn_format: '00' - Operator Identifier FQDN
- """
-
- plmn1 = enc_plmn(mcc, mnc) + epdg_priority + epdg_fqdn_format
- # TODO: Handle encoding of Length field for length more than 127 Bytes
- content = '80' + ('%02x' % (len(plmn1)//2)) + plmn1
- content = rpad(content, len(hexstr))
- return content
-
-
-def dec_ePDGSelection(sixhexbytes):
- """
- Decode ePDGSelection to get EF.ePDGSelection or EF.ePDGSelectionEm.
- See 3GPP TS 31.102 version 15.2.0 Release 15, section 4.2.104 and 4.2.106.
- """
-
- res = {'mcc': 0, 'mnc': 0, 'epdg_priority': 0, 'epdg_fqdn_format': ''}
- plmn_chars = 6
- epdg_priority_chars = 4
- epdg_fqdn_format_chars = 2
- # first three bytes (six ascii hex chars)
- plmn_str = sixhexbytes[:plmn_chars]
- # two bytes after first three bytes
- epdg_priority_str = sixhexbytes[plmn_chars:plmn_chars +
- epdg_priority_chars]
- # one byte after first five bytes
- epdg_fqdn_format_str = sixhexbytes[plmn_chars +
- epdg_priority_chars:plmn_chars + epdg_priority_chars + epdg_fqdn_format_chars]
- res['mcc'] = dec_mcc_from_plmn(plmn_str)
- res['mnc'] = dec_mnc_from_plmn(plmn_str)
- res['epdg_priority'] = epdg_priority_str
- res['epdg_fqdn_format'] = epdg_fqdn_format_str == '00' and 'Operator Identifier FQDN' or 'Location based FQDN'
- return res
-
-
-def format_ePDGSelection(hexstr):
- ePDGSelection_info_tag_chars = 2
- ePDGSelection_info_tag_str = hexstr[:2]
- s = ""
- # Minimum length
- len_chars = 2
- # TODO: Need to determine length properly - definite length support only
- # Inconsistency in spec: 3GPP TS 31.102 version 15.2.0 Release 15, 4.2.104
- # As per spec, length is 5n, n - number of PLMNs
- # But, each PLMN entry is made of PLMN (3 Bytes) + ePDG Priority (2 Bytes) + ePDG FQDN format (1 Byte)
- # Totalling to 6 Bytes, maybe length should be 6n
- len_str = hexstr[ePDGSelection_info_tag_chars:ePDGSelection_info_tag_chars+len_chars]
-
- # Not programmed scenario
- if int(len_str, 16) == 255 or int(ePDGSelection_info_tag_str, 16) == 255:
- len_chars = 0
- ePDGSelection_info_tag_chars = 0
- if len_str[0] == '8':
- # The bits 7 to 1 denotes the number of length octets if length > 127
- if int(len_str[1]) > 0:
- # Update number of length octets
- len_chars = len_chars * int(len_str[1])
- len_str = hexstr[ePDGSelection_info_tag_chars:len_chars]
-
- content_str = hexstr[ePDGSelection_info_tag_chars+len_chars:]
- # Right pad to prevent index out of range - multiple of 6 bytes
- content_str = rpad(content_str, len(content_str) +
- (12 - (len(content_str) % 12)))
- for rec_data in hexstr_to_Nbytearr(content_str, 6):
- rec_info = dec_ePDGSelection(rec_data)
- if rec_info['mcc'] == 0xFFF and rec_info['mnc'] == 0xFFF:
- rec_str = "unused"
- else:
- rec_str = "MCC: %03d MNC: %03d ePDG Priority: %s ePDG FQDN format: %s" % \
- (rec_info['mcc'], rec_info['mnc'],
- rec_info['epdg_priority'], rec_info['epdg_fqdn_format'])
- s += "\t%s # %s\n" % (rec_data, rec_str)
- return s
-
-
def get_addr_type(addr):
"""
Validates the given address and returns it's type (FQDN or IPv4 or IPv6)
@@ -1127,7 +871,7 @@ def get_addr_type(addr):
"""
# Empty address string
- if not len(addr):
+ if len(addr) == 0:
return None
addr_list = addr.split('.')
@@ -1142,7 +886,7 @@ def get_addr_type(addr):
return 0x01
elif ipa.version == 6:
return 0x02
- except Exception as e:
+ except Exception:
invalid_ipv4 = True
for i in addr_list:
# Invalid IPv4 may qualify for a valid FQDN, so make check here
@@ -1198,7 +942,7 @@ def tabulate_str_list(str_list, width: int = 79, hspace: int = 2, lspace: int =
Returns:
multi-line string containing formatted table
"""
- if str_list == None:
+ if str_list is None:
return ""
if len(str_list) <= 0:
return ""
@@ -1209,7 +953,7 @@ def tabulate_str_list(str_list, width: int = 79, hspace: int = 2, lspace: int =
table = []
for i in iter(range(rows)):
str_list_row = str_list[i::rows]
- if (align_left):
+ if align_left:
format_str_cell = '%%-%ds'
else:
format_str_cell = '%%%ds'
@@ -1223,6 +967,21 @@ def auto_int(x):
"""Helper function for argparse to accept hexadecimal integers."""
return int(x, 0)
+def _auto_uint(x, max_val: int):
+ """Helper function for argparse to accept hexadecimal or decimal integers."""
+ ret = int(x, 0)
+ if ret < 0 or ret > max_val:
+ raise argparse.ArgumentTypeError('Number exceeds permited value range (0, %u)' % max_val)
+ return ret
+
+def auto_uint7(x):
+ return _auto_uint(x, 127)
+
+def auto_uint8(x):
+ return _auto_uint(x, 255)
+
+def auto_uint16(x):
+ return _auto_uint(x, 65535)
def expand_hex(hexstring, length):
"""Expand a given hexstring to a specified length by replacing "." or ".."
@@ -1282,8 +1041,10 @@ class JsonEncoder(json.JSONEncoder):
"""Extend the standard library JSONEncoder with support for more types."""
def default(self, o):
- if isinstance(o, BytesIO) or isinstance(o, bytes) or isinstance(o, bytearray):
+ if isinstance(o, (BytesIO, bytes, bytearray)):
return b2h(o)
+ elif isinstance(o, datetime.datetime):
+ return o.isoformat()
return json.JSONEncoder.default(self, o)
@@ -1700,3 +1461,30 @@ class CardCommandSet:
def all_subclasses(cls) -> set:
"""Recursively get all subclasses of a specified class"""
return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)])
+
+def is_hexstr_or_decimal(instr: str) -> str:
+ """Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
+ [hexa]decimal digits only."""
+ if instr.isdecimal():
+ return instr
+ if not all(c in string.hexdigits for c in instr):
+ raise ValueError('Input must be [hexa]decimal')
+ if len(instr) & 1:
+ raise ValueError('Input has un-even number of hex digits')
+ return instr
+
+def is_hexstr(instr: str) -> str:
+ """Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
+ an even sequence of hexadecimal digits only."""
+ if not all(c in string.hexdigits for c in instr):
+ raise ValueError('Input must be hexadecimal')
+ if len(instr) & 1:
+ raise ValueError('Input has un-even number of hex digits')
+ return instr
+
+def is_decimal(instr: str) -> str:
+ """Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
+ an even sequence of decimal digits only."""
+ if not instr.isdecimal():
+ raise ValueError('Input must decimal')
+ return instr
diff --git a/pysim-testdata/Fairwaves-SIM.ok b/pysim-testdata/Fairwaves-SIM.ok
index 063fc6b..3751c57 100644
--- a/pysim-testdata/Fairwaves-SIM.ok
+++ b/pysim-testdata/Fairwaves-SIM.ok
@@ -31,7 +31,7 @@ OPLMNwAcT:
ffffff0000 # unused
HPLMNAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
diff --git a/pysim-testdata/Wavemobile-SIM.ok b/pysim-testdata/Wavemobile-SIM.ok
index 980332f..7a0ecec 100644
--- a/pysim-testdata/Wavemobile-SIM.ok
+++ b/pysim-testdata/Wavemobile-SIM.ok
@@ -11,7 +11,7 @@ Show in HPLMN: False
Hide in OPLMN: False
PLMNsel: 00f110ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
PLMNwAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -29,7 +29,7 @@ PLMNwAcT:
ffffff0000 # unused
OPLMNwAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
diff --git a/pysim-testdata/fakemagicsim.ok b/pysim-testdata/fakemagicsim.ok
index 805b360..4c366f9 100644
--- a/pysim-testdata/fakemagicsim.ok
+++ b/pysim-testdata/fakemagicsim.ok
@@ -1,7 +1,6 @@
Using PC/SC reader interface
Reading ...
Autodetected card type: fakemagicsim
-Can't read AIDs from SIM -- SW match failed! Expected 9000 and got 9404.
ICCID: 1122334455667788990
IMSI: 001010000000102
GID1: Can't read file -- SW match failed! Expected 9000 and got 9404.
diff --git a/pysim-testdata/pySim-trace_test_gsmtap.pcapng b/pysim-testdata/pySim-trace_test_gsmtap.pcapng
new file mode 100644
index 0000000..35c3003
--- /dev/null
+++ b/pysim-testdata/pySim-trace_test_gsmtap.pcapng
Binary files differ
diff --git a/pysim-testdata/pySim-trace_test_gsmtap.pcapng.ok b/pysim-testdata/pySim-trace_test_gsmtap.pcapng.ok
new file mode 100644
index 0000000..b947983
--- /dev/null
+++ b/pysim-testdata/pySim-trace_test_gsmtap.pcapng.ok
@@ -0,0 +1,618 @@
+Detected UICC Add-on "SIM"
+Detected UICC Add-on "GSM-R"
+Detected UICC Add-on "RUIM"
+Can't read AIDs from SIM -- 'list' object has no attribute 'lower'
+warning: EF.DIR seems to be empty!
+ ADF.ECASD: a0000005591010ffffffff8900000200
+ ADF.ISD-R: a0000005591010ffffffff8900000100
+ ISIM: a0000000871004
+ USIM: a0000000871002
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 READ BINARY MF/EF.PL - 9000 [None, None, None, None, None]
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 READ RECORD MF/EF.DIR 01 9000 {'application_template': [{'application_id': b'\xa0\x00\x00\x00\x87\x10\x02\xff\xff\xff\xff\x89\x07\t\x00\x00'}, {'application_label': 'USim1'}, {'discretionary_template': b'\xa0\x0c\x80\x01\x17\x81\x02_`\x82\x03EAP'}]}
+===============================
+00 READ RECORD MF/EF.DIR 02 9000 {'application_template': [{'application_id': b'\xa0\x00\x00\x00\x87\x10\x04\xff\xff\xff\xff\x89\x07\t\x00\x00'}, {'application_label': 'ISim1'}]}
+===============================
+00 READ RECORD MF/EF.DIR 03 9000 {'application_template': None}
+===============================
+00 READ RECORD MF/EF.DIR 04 9000 {'application_template': None}
+===============================
+00 READ RECORD MF/EF.DIR 05 9000 {'application_template': None}
+===============================
+00 READ RECORD MF/EF.DIR 06 9000 {'application_template': None}
+===============================
+00 READ RECORD MF/EF.DIR 07 9000 {'application_template': None}
+===============================
+00 READ RECORD MF/EF.DIR 08 9000 {'application_template': None}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ECC 01 9000 None
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ECC 02 9000 None
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ECC 03 9000 None
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ECC 04 9000 None
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ECC 05 9000 None
+===============================
+00 READ BINARY MF/ADF.USIM/EF.LI - 9000 [None, None, None, None, None]
+===============================
+00 UNBLOCK PIN - 63ca {'scope': 'global_mf', 'referenced_data_nr': 1, 'mode': 'check_remaining_attempts', 'remaining_attempts': 10}
+===============================
+00 VERIFY PIN - 63c3 {'scope': 'global_mf', 'referenced_data_nr': 1, 'mode': 'check_remaining_attempts', 'remaining_attempts': 3}
+===============================
+00 UNBLOCK PIN - 63ca {'scope': 'specific_df_adf', 'referenced_data_nr': 1, 'mode': 'check_remaining_attempts', 'remaining_attempts': 10}
+===============================
+00 VERIFY PIN - 63c3 {'scope': 'specific_df_adf', 'referenced_data_nr': 1, 'mode': 'check_remaining_attempts', 'remaining_attempts': 3}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.AD - 9000 {'ms_operation_mode': 'normal', 'additional_info': {'ciphering_indicator': False, 'csg_display_control': False, 'prose_services': False, 'extended_drx': False}, 'rfu': 0, 'mnc_len': 2, 'extensions': b''}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.IMSI - 9000 {'imsi': '901700000046734'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.ACC - 9000 {'ACC0': True, 'ACC1': False, 'ACC2': False, 'ACC3': False, 'ACC4': False, 'ACC5': False, 'ACC6': False, 'ACC7': False, 'ACC8': False, 'ACC9': False, 'ACC10': False, 'ACC11': False, 'ACC12': False, 'ACC13': False, 'ACC14': False, 'ACC15': False}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.LOCI - 9000 {'tmsi': 'ffffffff', 'lai': {'mcc_mnc': 'fff-ff', 'lac': 'fffe'}, 'rfu': 255, 'lu_status': 3}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.PSLOCI - 9000 {'ptmsi': 'ffffffff', 'ptmsi_sig': 'ffffff', 'rai': 'ffff00fffeff', 'rau_status': 2}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.UST - 9000 {1: {'activated': False, 'description': 'Local Phone Book'}, 2: {'activated': True, 'description': 'Fixed Dialling Numbers (FDN)'}, 3: {'activated': True, 'description': 'Extension 2'}, 4: {'activated': True, 'description': 'Service Dialling Numbers (SDN)'}, 5: {'activated': True, 'description': 'Extension3'}, 6: {'activated': True, 'description': 'Barred Dialling Numbers (BDN)'}, 7: {'activated': False, 'description': 'Extension4'}, 8: {'activated': True, 'description': 'Outgoing Call Information (OCI and OCT)'}, 9: {'activated': True, 'description': 'Incoming Call Information (ICI and ICT)'}, 10: {'activated': True, 'description': 'Short Message Storage (SMS)'}, 11: {'activated': True, 'description': 'Short Message Status Reports (SMSR)'}, 12: {'activated': True, 'description': 'Short Message Service Parameters (SMSP)'}, 13: {'activated': True, 'description': 'Advice of Charge (AoC)'}, 14: {'activated': True, 'description': 'Capability Configuration Parameters 2 (CCP2)'}, 15: {'activated': True, 'description': 'Cell Broadcast Message Identifier'}, 16: {'activated': True, 'description': 'Cell Broadcast Message Identifier Ranges'}, 17: {'activated': True, 'description': 'Group Identifier Level 1'}, 18: {'activated': True, 'description': 'Group Identifier Level 2'}, 19: {'activated': True, 'description': 'Service Provider Name'}, 20: {'activated': True, 'description': 'User controlled PLMN selector with Access Technology'}, 21: {'activated': True, 'description': 'MSISDN'}, 22: {'activated': False, 'description': 'Image (IMG)'}, 23: {'activated': False, 'description': 'Support of Localised Service Areas (SoLSA)'}, 24: {'activated': True, 'description': 'Enhanced Multi-Level Precedence and Pre-emption Service'}, 25: {'activated': True, 'description': 'Automatic Answer for eMLPP'}, 26: {'activated': False, 'description': 'RFU'}, 27: {'activated': True, 'description': 'GSM Access'}, 28: {'activated': True, 'description': 'Data download via SMS-PP'}, 29: {'activated': True, 'description': 'Data download via SMS-CB'}, 30: {'activated': False, 'description': 'Call Control by USIM'}, 31: {'activated': False, 'description': 'MO-SMS Control by USIM'}, 32: {'activated': True, 'description': 'RUN AT COMMAND command'}, 33: {'activated': True, 'description': 'shall be set to 1'}, 34: {'activated': True, 'description': 'Enabled Services Table'}, 35: {'activated': True, 'description': 'APN Control List (ACL)'}, 36: {'activated': False, 'description': 'Depersonalisation Control Keys'}, 37: {'activated': False, 'description': 'Co-operative Network List'}, 38: {'activated': True, 'description': 'GSM security context'}, 39: {'activated': True, 'description': 'CPBCCH Information'}, 40: {'activated': True, 'description': 'Investigation Scan'}, 41: {'activated': False, 'description': 'MexE'}, 42: {'activated': True, 'description': 'Operator controlled PLMN selector with Access Technology'}, 43: {'activated': True, 'description': 'HPLMN selector with Access Technology'}, 44: {'activated': True, 'description': 'Extension 5'}, 45: {'activated': True, 'description': 'PLMN Network Name'}, 46: {'activated': True, 'description': 'Operator PLMN List'}, 47: {'activated': False, 'description': 'Mailbox Dialling Numbers'}, 48: {'activated': False, 'description': 'Message Waiting Indication Status'}, 49: {'activated': False, 'description': 'Call Forwarding Indication Status'}, 50: {'activated': False, 'description': 'Reserved and shall be ignored'}, 51: {'activated': True, 'description': 'Service Provider Display Information'}, 52: {'activated': False, 'description': 'Multimedia Messaging Service (MMS)'}, 53: {'activated': False, 'description': 'Extension 8'}, 54: {'activated': False, 'description': 'Call control on GPRS by USIM'}, 55: {'activated': False, 'description': 'MMS User Connectivity Parameters'}, 56: {'activated': False, 'description': "Network's indication of alerting in the MS (NIA)"}, 57: {'activated': False, 'description': 'VGCS Group Identifier List (EFVGCS and EFVGCSS)'}, 58: {'activated': False, 'description': 'VBS Group Identifier List (EFVBS and EFVBSS)'}, 59: {'activated': False, 'description': 'Pseudonym'}, 60: {'activated': True, 'description': 'User Controlled PLMN selector for I-WLAN access'}, 61: {'activated': False, 'description': 'Operator Controlled PLMN selector for I-WLAN access'}, 62: {'activated': False, 'description': 'User controlled WSID list'}, 63: {'activated': False, 'description': 'Operator controlled WSID list'}, 64: {'activated': False, 'description': 'VGCS security'}, 65: {'activated': False, 'description': 'VBS security'}, 66: {'activated': False, 'description': 'WLAN Reauthentication Identity'}, 67: {'activated': False, 'description': 'Multimedia Messages Storage'}, 68: {'activated': False, 'description': 'Generic Bootstrapping Architecture (GBA)'}, 69: {'activated': False, 'description': 'MBMS security'}, 70: {'activated': False, 'description': 'Data download via USSD and USSD application mode'}, 71: {'activated': True, 'description': 'Equivalent HPLMN'}, 72: {'activated': False, 'description': 'Additional TERMINAL PROFILE after UICC activation'}, 73: {'activated': True, 'description': 'Equivalent HPLMN Presentation Indication'}, 74: {'activated': False, 'description': 'Last RPLMN Selection Indication'}, 75: {'activated': False, 'description': 'OMA BCAST Smart Card Profile'}, 76: {'activated': False, 'description': 'GBA-based Local Key Establishment Mechanism'}, 77: {'activated': False, 'description': 'Terminal Applications'}, 78: {'activated': False, 'description': 'Service Provider Name Icon'}, 79: {'activated': False, 'description': 'PLMN Network Name Icon'}, 80: {'activated': False, 'description': 'Connectivity Parameters for USIM IP connections'}, 81: {'activated': False, 'description': 'Home I-WLAN Specific Identifier List'}, 82: {'activated': False, 'description': 'I-WLAN Equivalent HPLMN Presentation Indication'}, 83: {'activated': False, 'description': 'I-WLAN HPLMN Priority Indication'}, 84: {'activated': False, 'description': 'I-WLAN Last Registered PLMN'}, 85: {'activated': True, 'description': 'EPS Mobility Management Information'}, 86: {'activated': True, 'description': 'Allowed CSG Lists and corresponding indications'}, 87: {'activated': True, 'description': 'Call control on EPS PDN connection by USIM'}, 88: {'activated': False, 'description': 'HPLMN Direct Access'}, 89: {'activated': True, 'description': 'eCall Data'}, 90: {'activated': True, 'description': 'Operator CSG Lists and corresponding indications'}, 91: {'activated': False, 'description': 'Support for SM-over-IP'}, 92: {'activated': False, 'description': 'Support of CSG Display Control'}, 93: {'activated': True, 'description': 'Communication Control for IMS by USIM'}, 94: {'activated': True, 'description': 'Extended Terminal Applications'}, 95: {'activated': False, 'description': 'Support of UICC access to IMS'}, 96: {'activated': False, 'description': 'Non-Access Stratum configuration by USIM'}, 97: {'activated': False, 'description': 'PWS configuration by USIM'}, 98: {'activated': False, 'description': 'RFU'}, 99: {'activated': False, 'description': 'URI support by UICC'}, 100: {'activated': False, 'description': 'Extended EARFCN support'}, 101: {'activated': False, 'description': 'ProSe'}, 102: {'activated': False, 'description': 'USAT Application Pairing'}, 103: {'activated': False, 'description': 'Media Type support'}, 104: {'activated': False, 'description': 'IMS call disconnection cause'}, 105: {'activated': False, 'description': 'URI support for MO SHORT MESSAGE CONTROL'}, 106: {'activated': False, 'description': 'ePDG configuration Information support'}, 107: {'activated': False, 'description': 'ePDG configuration Information configured'}, 108: {'activated': False, 'description': 'ACDC support'}, 109: {'activated': False, 'description': 'MCPTT'}, 110: {'activated': False, 'description': 'ePDG configuration Information for Emergency Service support'}, 111: {'activated': False, 'description': 'ePDG configuration Information for Emergency Service configured'}, 112: {'activated': False, 'description': 'eCall Data over IMS'}, 113: {'activated': False, 'description': 'URI support for SMS-PP DOWNLOAD as defined in 3GPP TS 31.111 [12]'}, 114: {'activated': False, 'description': 'From Preferred'}, 115: {'activated': False, 'description': 'IMS configuration data'}, 116: {'activated': False, 'description': 'TV configuration'}, 117: {'activated': False, 'description': '3GPP PS Data Off'}, 118: {'activated': False, 'description': '3GPP PS Data Off Service List'}, 119: {'activated': False, 'description': 'V2X'}, 120: {'activated': False, 'description': 'XCAP Configuration Data'}, 121: {'activated': False, 'description': 'EARFCN list for MTC/NB-IOT UEs'}, 122: {'activated': True, 'description': '5GS Mobility Management Information'}, 123: {'activated': True, 'description': '5G Security Parameters'}, 124: {'activated': True, 'description': 'Subscription identifier privacy support'}, 125: {'activated': False, 'description': 'SUCI calculation by the USIM'}, 126: {'activated': True, 'description': 'UAC Access Identities support'}, 127: {'activated': False, 'description': 'Expect control plane-based Steering of Roaming information during initial registration in VPLMN'}, 128: {'activated': False, 'description': 'Call control on PDU Session by USIM'}, 129: {'activated': False, 'description': '5GS Operator PLMN List'}, 130: {'activated': False, 'description': 'Support for SUPI of type NSI or GLI or GCI'}, 131: {'activated': False, 'description': '3GPP PS Data Off separate Home and Roaming lists'}, 132: {'activated': False, 'description': 'Support for URSP by USIM'}, 133: {'activated': False, 'description': '5G Security Parameters extended'}, 134: {'activated': False, 'description': 'MuD and MiD configuration data'}, 135: {'activated': False, 'description': 'Support for Trusted non-3GPP access networks by USIM'}, 136: {'activated': False, 'description': 'Support for multiple records of NAS security context storage for multiple registration'}, 137: {'activated': False, 'description': 'Pre-configured CAG information list'}, 138: {'activated': False, 'description': 'SOR-CMCI storage in USIM'}, 139: {'activated': False, 'description': '5G ProSe'}, 140: {'activated': False, 'description': 'Storage of disaster roaming information in USIM'}, 141: {'activated': False, 'description': 'Pre-configured eDRX parameters'}, 142: {'activated': False, 'description': '5G NSWO support'}, 143: {'activated': False, 'description': 'PWS configuration for SNPN in USIM'}, 144: {'activated': False, 'description': 'Multiplier Coefficient for Higher Priority PLMN search via NG-RAN satellite access'}, 145: {'activated': False, 'description': 'K_AUSF derivation configuration'}, 146: {'activated': False, 'description': 'Network Identifier for SNPN (NID)'}, 147: {'activated': False}, 148: {'activated': False}, 149: {'activated': False}, 150: {'activated': False}, 151: {'activated': False}, 152: {'activated': False}, 153: {'activated': False}, 154: {'activated': False}, 155: {'activated': False}, 156: {'activated': False}, 157: {'activated': False}, 158: {'activated': False}, 159: {'activated': False}, 160: {'activated': False}}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.EPSLOCI - 9000 {'guti': '0bf6ffffffffffffffffffff', 'last_visited_registered_tai': 'fffffffffe', 'eps_update_status': 'not_updated'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.Keys - 9000 {'ksi': 7, 'ck': 'ffffffffffffffffffffffffffffffff', 'ik': 'ffffffffffffffffffffffffffffffff'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.KeysPS - 9000 {'ksi': 7, 'ck': 'ffffffffffffffffffffffffffffffff', 'ik': 'ffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.EPSNSC 01 9000 {'eps_nas__security__context': [{'ksi_asme': 7}, {'k_asme': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, {'uplink_nas_count': 4294967295}, {'downlink_nas_count': 4294967295}, {'i_dof_nas_algorithms': '00'}]}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.START-HFN - 9000 {'start_cs': 0, 'start_ps': 0}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.THRESHOLD - 9000 {'max_start': 16777215}
+===============================
+00 READ BINARY MF/ADF.USIM/DF.GSM-ACCESS/EF.Kc - 9000 {'kc': 'ffffffffffffffff', 'cksn': 7}
+===============================
+00 READ BINARY MF/ADF.USIM/DF.GSM-ACCESS/EF.KcGPRS - 9000 {'kc': 'ffffffffffffffff', 'cksn': 7}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.EST - 9000 {1: {'activated': False, 'description': 'Fixed Dialling Numbers (FDN)'}, 2: {'activated': False, 'description': 'Barred Dialling Numbers (BDN)'}, 3: {'activated': False, 'description': 'APN Control List (ACL)'}, 4: {'activated': False}, 5: {'activated': False}, 6: {'activated': False}, 7: {'activated': False}, 8: {'activated': False}, 9: {'activated': False}, 10: {'activated': False}, 11: {'activated': False}, 12: {'activated': False}, 13: {'activated': False}, 14: {'activated': False}, 15: {'activated': False}, 16: {'activated': False}, 17: {'activated': False}, 18: {'activated': False}, 19: {'activated': False}, 20: {'activated': False}, 21: {'activated': False}, 22: {'activated': False}, 23: {'activated': False}, 24: {'activated': False}, 25: {'activated': False}, 26: {'activated': False}, 27: {'activated': False}, 28: {'activated': False}, 29: {'activated': False}, 30: {'activated': False}, 31: {'activated': False}, 32: {'activated': False}, 33: {'activated': False}, 34: {'activated': False}, 35: {'activated': False}, 36: {'activated': False}, 37: {'activated': False}, 38: {'activated': False}, 39: {'activated': False}, 40: {'activated': False}, 41: {'activated': False}, 42: {'activated': False}, 43: {'activated': False}, 44: {'activated': False}, 45: {'activated': False}, 46: {'activated': False}, 47: {'activated': False}, 48: {'activated': False}, 49: {'activated': False}, 50: {'activated': False}, 51: {'activated': False}, 52: {'activated': False}, 53: {'activated': False}, 54: {'activated': False}, 55: {'activated': False}, 56: {'activated': False}, 57: {'activated': False}, 58: {'activated': False}, 59: {'activated': False}, 60: {'activated': False}, 61: {'activated': False}, 62: {'activated': False}, 63: {'activated': False}, 64: {'activated': False}, 65: {'activated': False}, 66: {'activated': False}, 67: {'activated': False}, 68: {'activated': False}, 69: {'activated': False}, 70: {'activated': False}, 71: {'activated': False}, 72: {'activated': False}}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.GID1 - 9000 {'raw': 'ffffffffffffffffffff'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.GID2 - 9000 {'raw': 'ffffffffffffffffffff'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.NETPAR - 9000 {'raw': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.HPPLMN - 9000 5
+===============================
+00 READ BINARY MF/ADF.USIM/EF.HPLMNwAcT - 9000 [{'mcc': '001', 'mnc': '01', 'act': ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'GSM COMPACT', 'NG-RAN', 'UTRAN', 'cdma2000 1xRTT', 'cdma2000 HRPD']}, None, None, None, None, None, None, None, None, None, None, None]
+===============================
+00 READ BINARY MF/ADF.USIM/EF.PLMNwAcT - 9000 [{'mcc': '001', 'mnc': '01', 'act': ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'GSM COMPACT', 'NG-RAN', 'UTRAN', 'cdma2000 1xRTT', 'cdma2000 HRPD']}, None, None, None, None, None, None, None, None, None, None, None]
+===============================
+00 READ BINARY MF/ADF.USIM/EF.OPLMNwAcT - 9000 [{'mcc': '001', 'mnc': '01', 'act': ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'GSM COMPACT', 'NG-RAN', 'UTRAN', 'cdma2000 1xRTT', 'cdma2000 HRPD']}, None, None, None, None, None, None, None, None, None, None, None]
+===============================
+00 READ BINARY MF/ADF.USIM/EF.FPLMN - 9000 [{'mcc': '262', 'mnc': '01'}, {'mcc': '262', 'mnc': '02'}, {'mcc': '262', 'mnc': '03'}, None]
+===============================
+00 READ BINARY MF/ADF.USIM/EF.CBMID - 9000 [[65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535]]
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ACM 01 9000 {'raw': '000000'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.ACMmax - 9000 {'acm_max': 0}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.EHPLMN - 9000 [{'mcc': '001', 'mnc': '01'}, None, None, None]
+===============================
+00 READ BINARY MF/ADF.USIM/EF.EHPLMNPI - 9000 {'presentation_ind': 'display_all'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.EHPLMNPI - 9000 {'presentation_ind': 'no_preference'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.SPN - 9000 {'rfu': 0, 'hide_in_oplmn': True, 'show_in_hplmn': True, 'spn': 'Magic'}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.SPDI - 9000 {'raw': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 SEARCH RECORD MF/ADF.USIM/EF.PNN 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'sw': '9000'}}
+===============================
+00 SEARCH RECORD MF/ADF.USIM/EF.OPL 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffff'}, 'rsp': {'body': [1], 'sw': '9000'}}
+===============================
+00 GET STATUS - 9000 {'p1': 1, 'p2': 1, 'cmd': '8410a0000000871002ffffffff8907090000'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ARR 03 9000 [[{'access_mode': ['read_search_compare']}, {'control_reference_template': 'PIN1'}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record', 'update_erase']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.PBR 01 9000 {'raw': 'a81ec0034f3a01c1034f3202c3034f5414c5034f0904c6034f5212c9034f2109a90ac4034f1108ca034f500daa14c2034f4a03c7034f4b06c8034f5313cb034f4f16ffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.MSISDN 01 9000 {'msisdn': None}
+===============================
+00 UPDATE RECORD MF/ADF.USIM/EF.EPSNSC 01 9000 {'eps_nas__security__context': [{'ksi_asme': 7}, {'k_asme': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, {'uplink_nas_count': 4294967295}, {'downlink_nas_count': 4294967295}, {'i_dof_nas_algorithms': '00'}]}
+===============================
+00 UPDATE BINARY MF/ADF.USIM/EF.EPSLOCI - 9000 {'guti': '0bf6ffffffffffffffffffff', 'last_visited_registered_tai': 'fffffffffe', 'eps_update_status': 'not_updated'}
+===============================
+00 UPDATE RECORD MF/ADF.USIM/EF.EPSNSC 01 9000 {'eps_nas__security__context': [{'ksi_asme': 7}, {'k_asme': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, {'uplink_nas_count': 4294967295}, {'downlink_nas_count': 4294967295}, {'i_dof_nas_algorithms': '00'}]}
+===============================
+00 READ BINARY MF/ADF.USIM/EF.CBMI - 9000 [[65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535]]
+===============================
+00 READ BINARY MF/ADF.USIM/EF.CBMIR - 9000 [[{'lower': 65535, 'upper': 65535}], [{'lower': 65535, 'upper': 65535}], [{'lower': 65535, 'upper': 65535}], [{'lower': 65535, 'upper': 65535}], [{'lower': 65535, 'upper': 65535}]]
+===============================
+00 READ BINARY MF/ADF.USIM/EF.SMSS - 9000 {'last_used_tpmr': 255, 'memory_capacity_exceeded': False}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ARR 04 9000 [[{'access_mode': ['update_erase', 'read_search_compare']}, {'control_reference_template': 'PIN1'}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+00 SEARCH RECORD MF/ADF.USIM/EF.SMSP 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [2], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSP 01 9000 {'alpha_id': '', 'parameter_indicators': {'tp_dest_addr': False, 'tp_sc_addr': True, 'tp_pid': True, 'tp_dcs': True, 'tp_vp': True}, 'tp_dest_addr': {'length': 255, 'ton_npi': {'ext': True, 'type_of_number': 'reserved_for_extension', 'numbering_plan_id': 'reserved_for_extension'}, 'call_number': ''}, 'tp_sc_addr': {'length': 5, 'ton_npi': {'ext': True, 'type_of_number': 'unknown', 'numbering_plan_id': 'isdn_e164'}, 'call_number': '0015555f'}, 'tp_pid': '00', 'tp_dcs': '00', 'tp_vp_minutes': 5}
+===============================
+00 SEARCH RECORD MF/ADF.USIM/EF.SMS 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': '00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30], 'sw': '9000'}}
+===============================
+00 SEARCH RECORD MF/ADF.USIM/EF.SMSR 01 6a83 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': '00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'sw': '6a83'}}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 01 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 02 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 03 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 04 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 05 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 06 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 07 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 08 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 09 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 10 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 11 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 12 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 13 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 14 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 15 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 16 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 17 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 18 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 19 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SMSR 20 9000 {'sms_record_id': 255, 'sms_status_report': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
+===============================
+00 SEARCH RECORD MF/ADF.USIM/EF.FDN 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.FDN 01 9000 {'alpha_id': '', 'len_of_bcd': 255, 'ton_npi': {'ext': True, 'type_of_number': 'reserved_for_extension', 'numbering_plan_id': 'reserved_for_extension'}, 'dialing_nr': '', 'cap_conf_id': 255, 'ext2_record_id': 255}
+===============================
+00 SEARCH RECORD MF/ADF.USIM/EF.SDN 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.SDN 01 9000 {'alpha_id': '', 'len_of_bcd': 255, 'ton_npi': {'ext': True, 'type_of_number': 'reserved_for_extension', 'numbering_plan_id': 'reserved_for_extension'}, 'dialing_nr': '', 'cap_conf_id': 255, 'ext3_record_id': 255}
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/EF.FDN 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 'sw': '9000'}}
+===============================
+00 SEARCH RECORD MF/ADF.USIM/EF.MSISDN 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [2, 3, 4, 5, 6], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/EF.FDN 01 9000 {'alpha_id': '', 'len_of_bcd': 255, 'ton_npi': {'ext': True, 'type_of_number': 'reserved_for_extension', 'numbering_plan_id': 'reserved_for_extension'}, 'dialing_nr': '', 'cap_conf_id': 255, 'ext2_record_id': 255}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.MSISDN 01 9000 {'msisdn': None}
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/EF.SDN 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/EF.SDN 01 9000 {'alpha_id': '', 'len_of_bcd': 255, 'ton_npi': {'ext': True, 'type_of_number': 'reserved_for_extension', 'numbering_plan_id': 'reserved_for_extension'}, 'dialing_nr': '', 'cap_conf_id': 255, 'ext3_record_id': 255}
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK 01 6a83 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffff'}, 'rsp': {'sw': '6a83'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 01 9000 47726f75702031ffffffffffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 02 9000 47726f75702032ffffffffffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 03 9000 47726f75702033ffffffffffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 04 9000 47726f75702034ffffffffffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 05 9000 47726f75702035ffffffffffffffffff
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK 01 6a83 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffff'}, 'rsp': {'sw': '6a83'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 01 9000 576f726bffffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 02 9000 486f6d65ffffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 03 9000 4d6f62696c65ffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 04 9000 466178ffffffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 05 9000 4d61696effffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 06 9000 5061676572ffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 07 9000 456d61696cffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 08 9000 54656d70ffffffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 09 9000 4f6666696365ffffffff
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK 10 9000 4f7468657273ffffffff
+===============================
+00 READ BINARY MF/DF.TELECOM/DF.PHONEBOOK/EF.PSC - 9000 {'raw': '00000000'}
+===============================
+00 READ BINARY MF/DF.TELECOM/DF.PHONEBOOK/EF.PUID - 9000 {'raw': '0000'}
+===============================
+00 READ BINARY MF/DF.TELECOM/DF.PHONEBOOK/EF.CC - 9000 {'raw': '0000'}
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': '0000'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 0000
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': '0000'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 0000
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 ffffffffffffffffffffffffffffffffffff
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': '000000'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 000000
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 ffffffffffffffffffffffffffffffffff
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 ffff
+===============================
+00 SEARCH RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 {'cmd': {'file': 'currently_selected_ef', 'mode': 'forward_search', 'record_number': 1, 'search_string': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}, 'rsp': {'body': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250], 'sw': '9000'}}
+===============================
+00 READ RECORD MF/DF.TELECOM/DF.PHONEBOOK/EF.CC 01 9000 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+===============================
+01 UNBLOCK PIN - 63ca {'scope': 'global_mf', 'referenced_data_nr': 1, 'mode': 'check_remaining_attempts', 'remaining_attempts': 10}
+===============================
+01 VERIFY PIN - 63c3 {'scope': 'global_mf', 'referenced_data_nr': 1, 'mode': 'check_remaining_attempts', 'remaining_attempts': 3}
+===============================
+01 UNBLOCK PIN - 63ca {'scope': 'specific_df_adf', 'referenced_data_nr': 1, 'mode': 'check_remaining_attempts', 'remaining_attempts': 10}
+===============================
+01 VERIFY PIN - 63c3 {'scope': 'specific_df_adf', 'referenced_data_nr': 1, 'mode': 'check_remaining_attempts', 'remaining_attempts': 3}
+===============================
+00 READ RECORD MF/EF.ARR 02 9000 [[{'access_mode': ['read_search_compare']}, {'always': None}], [{'access_mode': ['update_erase']}, {'never': None}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+00 READ RECORD MF/ADF.USIM/EF.ARR 06 9000 [[{'access_mode': ['read_search_compare']}, {'always': None}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record', 'update_erase']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.ARR 03 9000 [[{'access_mode': ['read_search_compare']}, {'control_reference_template': 'PIN1'}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record', 'update_erase']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.ARR 03 9000 [[{'access_mode': ['read_search_compare']}, {'control_reference_template': 'PIN1'}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record', 'update_erase']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'close_channel', 'closed_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.ARR 03 9000 [[{'access_mode': ['read_search_compare']}, {'control_reference_template': 'PIN1'}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record', 'update_erase']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.ARR 03 9000 [[{'access_mode': ['read_search_compare']}, {'control_reference_template': 'PIN1'}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record', 'update_erase']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.ARR 03 9000 [[{'access_mode': ['read_search_compare']}, {'control_reference_template': 'PIN1'}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record', 'update_erase']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.ARR 04 9000 [[{'access_mode': ['update_erase', 'read_search_compare']}, {'control_reference_template': 'PIN1'}], [{'access_mode': ['activate_file_or_record', 'deactivate_file_or_record']}, {'control_reference_template': 'ADM1'}], [{'command_header': {'INS': 212}}, {'control_reference_template': 'ADM1'}]]
+===============================
+00 READ RECORD MF/ADF.USIM/EF.MBI 01 9000 {'mbi_voicemail': 255, 'mbi_fax': 255, 'mbi_email': 255, 'mbi_other': 255, 'mbi_videocall': None}
+===============================
+00 READ RECORD MF/DF.TELECOM 01 9000 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+===============================
+01 READ BINARY MF/ADF.ISIM/EF.IMPI - 9000 {'nai': ''}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.IMPU 01 9000 {'impu': ''}
+===============================
+01 READ BINARY MF/ADF.ISIM/EF.DOMAIN - 9000 {'domain': ''}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.P-CSCF 01 9000 {'pcscf_address': None}
+===============================
+01 READ BINARY MF/ADF.ISIM/EF.IST - 9000 {1: {'activated': True, 'description': 'P-CSCF address'}, 2: {'activated': False, 'description': 'Generic Bootstrapping Architecture (GBA)'}, 3: {'activated': False, 'description': 'HTTP Digest'}, 4: {'activated': True, 'description': 'GBA-based Local Key Establishment Mechanism'}, 5: {'activated': True, 'description': 'Support of P-CSCF discovery for IMS Local Break Out'}, 6: {'activated': False, 'description': 'Short Message Storage (SMS)'}, 7: {'activated': False, 'description': 'Short Message Status Reports (SMSR)'}, 8: {'activated': False, 'description': 'Support for SM-over-IP including data download via SMS-PP as defined in TS 31.111 [31]'}, 9: {'activated': False, 'description': 'Communication Control for IMS by ISIM'}, 10: {'activated': True, 'description': 'Support of UICC access to IMS'}, 11: {'activated': False, 'description': 'URI support by UICC'}, 12: {'activated': False, 'description': 'Media Type support'}, 13: {'activated': False, 'description': 'IMS call disconnection cause'}, 14: {'activated': False, 'description': 'URI support for MO SHORT MESSAGE CONTROL'}, 15: {'activated': False, 'description': 'MCPTT'}, 16: {'activated': False, 'description': 'URI support for SMS-PP DOWNLOAD as defined in 3GPP TS 31.111 [31]'}, 17: {'activated': False, 'description': 'From Preferred'}, 18: {'activated': False, 'description': 'IMS configuration data'}, 19: {'activated': False, 'description': 'XCAP Configuration Data'}, 20: {'activated': False, 'description': 'WebRTC URI'}, 21: {'activated': False, 'description': 'MuD and MiD configuration data'}, 22: {'activated': False}, 23: {'activated': False}, 24: {'activated': False}}
+===============================
+01 READ BINARY MF/ADF.ISIM/EF.GBABP - 9000 None
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.IMPU 02 9000 {'impu': None}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.P-CSCF 02 9000 {'pcscf_address': {'type_of_address': 255, 'address': None}}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 READ RECORD MF/ADF.USIM/EF.MSISDN 01 9000 {'msisdn': None}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.IMPU 03 9000 {'impu': None}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.P-CSCF 03 9000 {'pcscf_address': {'type_of_address': 255, 'address': None}}
+===============================
+00 UPDATE BINARY MF/ADF.USIM/EF.CBMI - 9000 [[65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535], [65535]]
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'close_channel', 'closed_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.IMPU 04 9000 {'impu': None}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.P-CSCF 04 9000 {'pcscf_address': {'type_of_address': 255, 'address': None}}
+===============================
+00 UPDATE BINARY MF/ADF.USIM/EF.CBMIR - 9000 [[{'lower': 65535, 'upper': 65535}], [{'lower': 65535, 'upper': 65535}], [{'lower': 65535, 'upper': 65535}], [{'lower': 65535, 'upper': 65535}], [{'lower': 65535, 'upper': 65535}]]
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.IMPU 05 9000 {'impu': None}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.P-CSCF 05 9000 {'pcscf_address': {'type_of_address': 255, 'address': None}}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.IMPU 06 9000 {'impu': None}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.P-CSCF 06 9000 {'pcscf_address': {'type_of_address': 255, 'address': None}}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.IMPU 07 9000 {'impu': None}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.P-CSCF 07 9000 {'pcscf_address': {'type_of_address': 255, 'address': None}}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.IMPU 08 9000 {'impu': None}
+===============================
+01 READ RECORD MF/ADF.ISIM/EF.P-CSCF 08 9000 {'pcscf_address': {'type_of_address': 255, 'address': None}}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'close_channel', 'closed_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'close_channel', 'closed_channel': 2}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'close_channel', 'closed_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'close_channel', 'closed_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'close_channel', 'closed_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'open_channel', 'created_channel': 2}
+===============================
+00 MANAGE CHANNEL 02 9000 {'mode': 'close_channel', 'closed_channel': 2}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+CardReset(3b9f96801f878031e073fe211b674a4c753034054ba9)
+===============================
+00 READ BINARY MF/EF.ICCID - 9000 {'iccid': '8988211000000465008'}
+===============================
+00 TERMINAL PROFILE - 9000 {'p1': 0, 'p2': 0, 'cmd': 'ffffffff7f9d00dfbf00001fe2000000c36b000700004000500000000008'}
+===============================
+00 MANAGE CHANNEL 01 9000 {'mode': 'open_channel', 'created_channel': 1}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+00 GET STATUS - 9000 {'p1': 0, 'p2': 12}
+===============================
+682 APDUs parsed, stop iteration.
diff --git a/pysim-testdata/sysmoISIM-SJA2.ok b/pysim-testdata/sysmoISIM-SJA2.ok
index 45acd79..5640171 100644
--- a/pysim-testdata/sysmoISIM-SJA2.ok
+++ b/pysim-testdata/sysmoISIM-SJA2.ok
@@ -11,7 +11,7 @@ Show in HPLMN: True
Hide in OPLMN: True
PLMNsel: 00f110ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
PLMNwAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -25,7 +25,7 @@ PLMNwAcT:
ffffff0000 # unused
OPLMNwAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -39,7 +39,7 @@ OPLMNwAcT:
ffffff0000 # unused
HPLMNAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
diff --git a/pysim-testdata/sysmoUSIM-SJS1.ok b/pysim-testdata/sysmoUSIM-SJS1.ok
index b793cc0..c909a89 100644
--- a/pysim-testdata/sysmoUSIM-SJS1.ok
+++ b/pysim-testdata/sysmoUSIM-SJS1.ok
@@ -11,7 +11,7 @@ Show in HPLMN: True
Hide in OPLMN: True
PLMNsel: 00f110ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
PLMNwAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -25,7 +25,7 @@ PLMNwAcT:
ffffff0000 # unused
OPLMNwAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
@@ -39,7 +39,7 @@ OPLMNwAcT:
ffffff0000 # unused
HPLMNAcT:
- 00f110ffff # MCC: 001 MNC: 01 AcT: UTRAN, E-UTRAN WB-S1, E-UTRAN NB-S1, NG-RAN, GSM, GSM COMPACT, cdma2000 HRPD, cdma2000 1xRTT
+ 00f110ffff # MCC: 001 MNC: 01 AcT: E-UTRAN NB-S1, E-UTRAN WB-S1, EC-GSM-IoT, GSM, GSM COMPACT, NG-RAN, UTRAN, cdma2000 1xRTT, cdma2000 HRPD
ffffff0000 # unused
ffffff0000 # unused
ffffff0000 # unused
diff --git a/pysim-testdata/sysmosim-gr1.ok b/pysim-testdata/sysmosim-gr1.ok
index 60c568b..70ac763 100644
--- a/pysim-testdata/sysmosim-gr1.ok
+++ b/pysim-testdata/sysmosim-gr1.ok
@@ -1,7 +1,6 @@
Using PC/SC reader interface
Reading ...
Autodetected card type: sysmosim-gr1
-Can't read AIDs from SIM -- SW match failed! Expected 9000 and got 9404.
ICCID: 1122334455667788990
IMSI: 001010000000102
GID1: Can't read file -- SW match failed! Expected 9000 and got 9404.
diff --git a/requirements.txt b/requirements.txt
index 4332b66..ec04b6e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,5 +10,8 @@ pyyaml>=5.1
termcolor
colorlog
pycryptodomex
+cryptography
+git+https://github.com/osmocom/asn1tools
packaging
git+https://github.com/hologram-io/smpp.pdu
+git+https://github.com/jookies/smpp.twisted
diff --git a/setup.py b/setup.py
index 5678c55..3b16ff2 100644
--- a/setup.py
+++ b/setup.py
@@ -3,14 +3,22 @@ from setuptools import setup
setup(
name='pySim',
version='1.0',
- packages=['pySim', 'pySim.transport', 'pySim.apdu', 'pySim.apdu_source'],
+ packages=[
+ 'pySim',
+ 'pySim.apdu',
+ 'pySim.apdu_source',
+ 'pySim.esim',
+ 'pySim.global_platform',
+ 'pySim.legacy',
+ 'pySim.transport',
+ ],
url='https://osmocom.org/projects/pysim/wiki',
license='GPLv2',
author_email='simtrace@lists.osmocom.org',
description='Tools related to SIM/USIM/ISIM cards',
install_requires=[
"pyscard",
- "serial",
+ "pyserial",
"pytlv",
"cmd2 >= 1.5.0",
"jsonpath-ng",
@@ -23,6 +31,7 @@ setup(
"pycryptodomex",
"packaging",
"smpp.pdu @ git+https://github.com/hologram-io/smpp.pdu",
+ "smpp.twisted @ git+https://github.com/jookies/smpp.twisted",
],
scripts=[
'pySim-prog.py',
diff --git a/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_BRP.der b/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_BRP.der
new file mode 100644
index 0000000..98942c1
--- /dev/null
+++ b/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_BRP.pem b/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_BRP.pem
new file mode 100644
index 0000000..4c72371
--- /dev/null
+++ b/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_BRP.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICUTCCAfigAwIBAgIJALh086v6bETTMAoGCCqGSM49BAMCMEQxEDAOBgNVBAMM
+B1Rlc3QgQ0kxETAPBgNVBAsMCFRFU1RDRVJUMRAwDgYDVQQKDAdSU1BURVNUMQsw
+CQYDVQQGEwJJVDAgFw0yMDA0MDEwODI3NTFaGA8yMDU1MDQwMTA4Mjc1MVowRDEQ
+MA4GA1UEAwwHVGVzdCBDSTERMA8GA1UECwwIVEVTVENFUlQxEDAOBgNVBAoMB1JT
+UFRFU1QxCzAJBgNVBAYTAklUMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABCeH
+tNVu2CSp5r4E4Yh/a5i6/rjHY/UoN/cBE+k2Tt2+E5vAx95+Fo8eXNDBhTT8UGTm
+T2htxTMnyn8dzqhaKZSjgc8wgcwwHQYDVR0OBBYEFMC8cLo2kp1DtGf/V1cFMOV6
+uPzYMA8GA1UdEwEB/wQFMAMBAf8wFwYDVR0gAQH/BA0wCzAJBgdngRIBAgEAMA4G
+A1UdDwEB/wQEAwIBBjAOBgNVHREEBzAFiAOINwEwYQYDVR0fBFowWDAqoCigJoYk
+aHR0cDovL2NpLnRlc3QuZXhhbXBsZS5jb20vQ1JMLUEuY3JsMCqgKKAmhiRodHRw
+Oi8vY2kudGVzdC5leGFtcGxlLmNvbS9DUkwtQi5jcmwwCgYIKoZIzj0EAwIDRwAw
+RAIgPYrf0CKl0FBMUaHx5xS1duTDbQ4wBZN3qKBeNniuux0CIHBek2vLfoANAdtt
+f5u5Ce6DVC2oIfpn5UnS24F3oMqM
+-----END CERTIFICATE-----
diff --git a/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_NIST.der b/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_NIST.der
new file mode 100644
index 0000000..22bc277
--- /dev/null
+++ b/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_NIST.pem b/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_NIST.pem
new file mode 100644
index 0000000..9f6f4cd
--- /dev/null
+++ b/smdpp-data/certs/CertificateIssuer/CERT_CI_ECDSA_NIST.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICUDCCAfegAwIBAgIJALh086v6bETTMAoGCCqGSM49BAMCMEQxEDAOBgNVBAMM
+B1Rlc3QgQ0kxETAPBgNVBAsMCFRFU1RDRVJUMRAwDgYDVQQKDAdSU1BURVNUMQsw
+CQYDVQQGEwJJVDAgFw0yMDA0MDEwODI3NTFaGA8yMDU1MDQwMTA4Mjc1MVowRDEQ
+MA4GA1UEAwwHVGVzdCBDSTERMA8GA1UECwwIVEVTVENFUlQxEDAOBgNVBAoMB1JT
+UFRFU1QxCzAJBgNVBAYTAklUMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElAZX
+pnPcKI+J1S6opHcEmSeR+cNLADbmM+LQy6lFTWXbMusXmBeZ0vJDiO4rlcEJRUbJ
+eQHOrrqWUJGaLiDSKaOBzzCBzDAdBgNVHQ4EFgQU9UFyvfmKldZcvriKOKHBHYAK
+hcMwDwYDVR0TAQH/BAUwAwEB/zAXBgNVHSABAf8EDTALMAkGB2eBEgECAQAwDgYD
+VR0PAQH/BAQDAgEGMA4GA1UdEQQHMAWIA4g3ATBhBgNVHR8EWjBYMCqgKKAmhiRo
+dHRwOi8vY2kudGVzdC5leGFtcGxlLmNvbS9DUkwtQS5jcmwwKqAooCaGJGh0dHA6
+Ly9jaS50ZXN0LmV4YW1wbGUuY29tL0NSTC1CLmNybDAKBggqhkjOPQQDAgNHADBE
+AiBSdWqvwgIKbOy/Ll88IIklEP8pdR0pi9OwFdlgWk/mfQIgV5goNuTSBd3S5sPB
+tFWTf2tuSTtgL9G2bDV0iak192s=
+-----END CERTIFICATE-----
diff --git a/smdpp-data/certs/CertificateIssuer/CI-csr.cnf b/smdpp-data/certs/CertificateIssuer/CI-csr.cnf
new file mode 100644
index 0000000..89fdf6d
--- /dev/null
+++ b/smdpp-data/certs/CertificateIssuer/CI-csr.cnf
@@ -0,0 +1,25 @@
+#openssl x509 extfile params
+extensions = extend
+# This prevent the user to be prompted for values
+prompt = no
+
+distinguished_name = dn-param
+[dn-param] # DN fields
+CN = Test CI
+OU = TESTCERT
+O = RSPTEST
+C = IT
+
+# Extensions for the Test CI
+[extend] # openssl extensions
+subjectKeyIdentifier = hash
+
+basicConstraints = critical, CA:true
+
+certificatePolicies=critical,2.23.146.1.2.1.0
+
+keyUsage =critical, keyCertSign, cRLSign
+
+subjectAltName = RID:2.999.1
+
+crlDistributionPoints=URI:http://ci.test.example.com/CRL-A.crl, URI:http://ci.test.example.com/CRL-B.crl \ No newline at end of file
diff --git a/smdpp-data/certs/DPauth/CERT_S_SM_DP2auth_ECDSA_BRP.der b/smdpp-data/certs/DPauth/CERT_S_SM_DP2auth_ECDSA_BRP.der
new file mode 100644
index 0000000..a3caae5
--- /dev/null
+++ b/smdpp-data/certs/DPauth/CERT_S_SM_DP2auth_ECDSA_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/DPauth/CERT_S_SM_DP2auth_ECDSA_NIST.der b/smdpp-data/certs/DPauth/CERT_S_SM_DP2auth_ECDSA_NIST.der
new file mode 100644
index 0000000..81715bb
--- /dev/null
+++ b/smdpp-data/certs/DPauth/CERT_S_SM_DP2auth_ECDSA_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/DPauth/CERT_S_SM_DPauth_ECDSA_BRP.der b/smdpp-data/certs/DPauth/CERT_S_SM_DPauth_ECDSA_BRP.der
new file mode 100644
index 0000000..45c043c
--- /dev/null
+++ b/smdpp-data/certs/DPauth/CERT_S_SM_DPauth_ECDSA_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/DPauth/CERT_S_SM_DPauth_ECDSA_NIST.der b/smdpp-data/certs/DPauth/CERT_S_SM_DPauth_ECDSA_NIST.der
new file mode 100644
index 0000000..914b8a1
--- /dev/null
+++ b/smdpp-data/certs/DPauth/CERT_S_SM_DPauth_ECDSA_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/DPauth/PK_S_SM_DP2auth_ECDSA_BRP.pem b/smdpp-data/certs/DPauth/PK_S_SM_DP2auth_ECDSA_BRP.pem
new file mode 100644
index 0000000..e37c621
--- /dev/null
+++ b/smdpp-data/certs/DPauth/PK_S_SM_DP2auth_ECDSA_BRP.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABC7uB8ltAFvlGV95rR2tzR03jKMJ
+XkT3LupBDwjHVhIskGPJZbf8hSnAyk6bT2WWnCg8ZWNkV4dxGPbRFy1qI2U=
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPauth/PK_S_SM_DP2auth_ECDSA_NIST.pem b/smdpp-data/certs/DPauth/PK_S_SM_DP2auth_ECDSA_NIST.pem
new file mode 100644
index 0000000..63fc92d
--- /dev/null
+++ b/smdpp-data/certs/DPauth/PK_S_SM_DP2auth_ECDSA_NIST.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKy6bRdNRwJr0DpFDg6GQOEfyYH3m
+DverJcQOA/jbtlCOFFdQjCvgAXNH7Pob+fd159B+gF5S4ZiLe1hacRGuMw==
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPauth/PK_S_SM_DPauth_ECDSA_BRP.pem b/smdpp-data/certs/DPauth/PK_S_SM_DPauth_ECDSA_BRP.pem
new file mode 100644
index 0000000..cbd8ccc
--- /dev/null
+++ b/smdpp-data/certs/DPauth/PK_S_SM_DPauth_ECDSA_BRP.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABCVdfa8A74Qddvp6Y9Y+s/5sn3BJ
+LTgZI/j4vXsk88WtFo6+OwndgPKef/0kpNG+dH+DI+RykoOW3engFRzquhg=
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPauth/PK_S_SM_DPauth_ECDSA_NIST.pem b/smdpp-data/certs/DPauth/PK_S_SM_DPauth_ECDSA_NIST.pem
new file mode 100644
index 0000000..8424bb4
--- /dev/null
+++ b/smdpp-data/certs/DPauth/PK_S_SM_DPauth_ECDSA_NIST.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETf7U9GlHkb8Wlc6gMHo1tBgBlpU4
+e7dbfSRHtrUgnwRFrk5eUhzROIjXX+B8hYAiKuINuqwdd812MEmTQhvXOQ==
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPauth/SK_S_SM_DP2auth_ECDSA_BRP.pem b/smdpp-data/certs/DPauth/SK_S_SM_DP2auth_ECDSA_BRP.pem
new file mode 100644
index 0000000..dc20882
--- /dev/null
+++ b/smdpp-data/certs/DPauth/SK_S_SM_DP2auth_ECDSA_BRP.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BgkrJAMDAggBAQc=
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHgCAQEEIAwXNVwBHQ/o19rdY/GXhc9sUcvNRmroi+j4G8EFiEb2oAsGCSskAwMC
+CAEBB6FEA0IABC7uB8ltAFvlGV95rR2tzR03jKMJXkT3LupBDwjHVhIskGPJZbf8
+hSnAyk6bT2WWnCg8ZWNkV4dxGPbRFy1qI2U=
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPauth/SK_S_SM_DP2auth_ECDSA_NIST.pem b/smdpp-data/certs/DPauth/SK_S_SM_DP2auth_ECDSA_NIST.pem
new file mode 100644
index 0000000..0b19301
--- /dev/null
+++ b/smdpp-data/certs/DPauth/SK_S_SM_DP2auth_ECDSA_NIST.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIJwyoJXUiELZ/6QE9xJRKqLFQloaJjhqtqFF1YEeA5FBoAoGCCqGSM49
+AwEHoUQDQgAEKy6bRdNRwJr0DpFDg6GQOEfyYH3mDverJcQOA/jbtlCOFFdQjCvg
+AXNH7Pob+fd159B+gF5S4ZiLe1hacRGuMw==
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPauth/SK_S_SM_DPauth_ECDSA_BRP.pem b/smdpp-data/certs/DPauth/SK_S_SM_DPauth_ECDSA_BRP.pem
new file mode 100644
index 0000000..6e9d064
--- /dev/null
+++ b/smdpp-data/certs/DPauth/SK_S_SM_DPauth_ECDSA_BRP.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BgkrJAMDAggBAQc=
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHgCAQEEIJP7M9BYTzSbB/i10q+T18PjVLNJo7kTUC5qvAcOTUkpoAsGCSskAwMC
+CAEBB6FEA0IABCVdfa8A74Qddvp6Y9Y+s/5sn3BJLTgZI/j4vXsk88WtFo6+Ownd
+gPKef/0kpNG+dH+DI+RykoOW3engFRzquhg=
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPauth/SK_S_SM_DPauth_ECDSA_NIST.pem b/smdpp-data/certs/DPauth/SK_S_SM_DPauth_ECDSA_NIST.pem
new file mode 100644
index 0000000..35df74b
--- /dev/null
+++ b/smdpp-data/certs/DPauth/SK_S_SM_DPauth_ECDSA_NIST.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIAp8wcJE5gxSzVt4B6uMNgwmUkYBUH3KvF3VmLWmFtXVoAoGCCqGSM49
+AwEHoUQDQgAETf7U9GlHkb8Wlc6gMHo1tBgBlpU4e7dbfSRHtrUgnwRFrk5eUhzR
+OIjXX+B8hYAiKuINuqwdd812MEmTQhvXOQ==
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPauth/data_sig.der b/smdpp-data/certs/DPauth/data_sig.der
new file mode 100644
index 0000000..7f44ce2
--- /dev/null
+++ b/smdpp-data/certs/DPauth/data_sig.der
@@ -0,0 +1 @@
+0D AL¶þV¿eRÌÍìAˆHÊt£×ôͺ„nìE<Nåû R¤~&Àk\þ~­ ÉRlÜÛ°Ÿ‰¥7ì¶NŒŽmWø \ No newline at end of file
diff --git a/smdpp-data/certs/DPpb/CERT_S_SM_DP2pb_ECDSA_BRP.der b/smdpp-data/certs/DPpb/CERT_S_SM_DP2pb_ECDSA_BRP.der
new file mode 100644
index 0000000..a382a5b
--- /dev/null
+++ b/smdpp-data/certs/DPpb/CERT_S_SM_DP2pb_ECDSA_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/DPpb/CERT_S_SM_DP2pb_ECDSA_NIST.der b/smdpp-data/certs/DPpb/CERT_S_SM_DP2pb_ECDSA_NIST.der
new file mode 100644
index 0000000..11a1339
--- /dev/null
+++ b/smdpp-data/certs/DPpb/CERT_S_SM_DP2pb_ECDSA_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/DPpb/CERT_S_SM_DPpb_ECDSA_BRP.der b/smdpp-data/certs/DPpb/CERT_S_SM_DPpb_ECDSA_BRP.der
new file mode 100644
index 0000000..089230b
--- /dev/null
+++ b/smdpp-data/certs/DPpb/CERT_S_SM_DPpb_ECDSA_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/DPpb/CERT_S_SM_DPpb_ECDSA_NIST.der b/smdpp-data/certs/DPpb/CERT_S_SM_DPpb_ECDSA_NIST.der
new file mode 100644
index 0000000..3b585e8
--- /dev/null
+++ b/smdpp-data/certs/DPpb/CERT_S_SM_DPpb_ECDSA_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/DPpb/PK_S_SM_DP2pb_ECDSA_BRP.pem b/smdpp-data/certs/DPpb/PK_S_SM_DP2pb_ECDSA_BRP.pem
new file mode 100644
index 0000000..c6007b6
--- /dev/null
+++ b/smdpp-data/certs/DPpb/PK_S_SM_DP2pb_ECDSA_BRP.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABHNhhV5ZAmSyUscaYGKgIeJ7Rvdg
+uBOCBqf3RCDvi8Vai5iNFlj51w06LYSAqt9k5ixNcSfeWgorkXqUP0R0qt0=
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPpb/PK_S_SM_DP2pb_ECDSA_NIST.pem b/smdpp-data/certs/DPpb/PK_S_SM_DP2pb_ECDSA_NIST.pem
new file mode 100644
index 0000000..b8c30e2
--- /dev/null
+++ b/smdpp-data/certs/DPpb/PK_S_SM_DP2pb_ECDSA_NIST.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPi11oQYiEhbCruj61YT3eWM8+URo
+G17HWpZlP19AHEdq451UbAMtMtAHCnuG5dzrqt3zHKXCdmwZa9ebwhoBgA==
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPpb/PK_S_SM_DPpb_ECDSA_BRP.pem b/smdpp-data/certs/DPpb/PK_S_SM_DPpb_ECDSA_BRP.pem
new file mode 100644
index 0000000..95c77bf
--- /dev/null
+++ b/smdpp-data/certs/DPpb/PK_S_SM_DPpb_ECDSA_BRP.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABIkyU+zJTFHfT+5uDbCVHPxlYnSB
+tUGOVXBp9If6oVRmpcuOEMwrOAmc8qkp9/gtrAZRJrcdBkCWHbf5GknpVhI=
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPpb/PK_S_SM_DPpb_ECDSA_NIST.pem b/smdpp-data/certs/DPpb/PK_S_SM_DPpb_ECDSA_NIST.pem
new file mode 100644
index 0000000..fabd554
--- /dev/null
+++ b/smdpp-data/certs/DPpb/PK_S_SM_DPpb_ECDSA_NIST.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEEwq49At75yXkmGnxnEAdrlwch0J
+VaJkSuBfrkvCMU5dwJ9r8BGAJhZTQuISMYd142XztXMlNzBmuZBuC9E4jQ==
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPpb/SK_S_SM_DP2pb_ECDSA_BRP.pem b/smdpp-data/certs/DPpb/SK_S_SM_DP2pb_ECDSA_BRP.pem
new file mode 100644
index 0000000..1e1b437
--- /dev/null
+++ b/smdpp-data/certs/DPpb/SK_S_SM_DP2pb_ECDSA_BRP.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BgkrJAMDAggBAQc=
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHgCAQEEIJyuLhpWB6nVeDgu7pMuJR9SME+G7rHxcIzb08B74s09oAsGCSskAwMC
+CAEBB6FEA0IABHNhhV5ZAmSyUscaYGKgIeJ7RvdguBOCBqf3RCDvi8Vai5iNFlj5
+1w06LYSAqt9k5ixNcSfeWgorkXqUP0R0qt0=
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPpb/SK_S_SM_DP2pb_ECDSA_NIST.pem b/smdpp-data/certs/DPpb/SK_S_SM_DP2pb_ECDSA_NIST.pem
new file mode 100644
index 0000000..63045bd
--- /dev/null
+++ b/smdpp-data/certs/DPpb/SK_S_SM_DP2pb_ECDSA_NIST.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIGaTEUljnbqsHcPTBsWL0t/SL3O/Y6yGMZgykLV/kJNFoAoGCCqGSM49
+AwEHoUQDQgAEPi11oQYiEhbCruj61YT3eWM8+URoG17HWpZlP19AHEdq451UbAMt
+MtAHCnuG5dzrqt3zHKXCdmwZa9ebwhoBgA==
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPpb/SK_S_SM_DPpb_ECDSA_BRP.pem b/smdpp-data/certs/DPpb/SK_S_SM_DPpb_ECDSA_BRP.pem
new file mode 100644
index 0000000..9da501d
--- /dev/null
+++ b/smdpp-data/certs/DPpb/SK_S_SM_DPpb_ECDSA_BRP.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BgkrJAMDAggBAQc=
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHgCAQEEIHX/Mi9BZhba4aSE73HUh0+w3zKV/TXCy6SJ+7K7nHv2oAsGCSskAwMC
+CAEBB6FEA0IABIkyU+zJTFHfT+5uDbCVHPxlYnSBtUGOVXBp9If6oVRmpcuOEMwr
+OAmc8qkp9/gtrAZRJrcdBkCWHbf5GknpVhI=
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPpb/SK_S_SM_DPpb_ECDSA_NIST.pem b/smdpp-data/certs/DPpb/SK_S_SM_DPpb_ECDSA_NIST.pem
new file mode 100644
index 0000000..9595c07
--- /dev/null
+++ b/smdpp-data/certs/DPpb/SK_S_SM_DPpb_ECDSA_NIST.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEINzWlLd4lX6Omt292UQz6e+Pc9EeSRxI1CWjipSRvTvtoAoGCCqGSM49
+AwEHoUQDQgAEEEwq49At75yXkmGnxnEAdrlwch0JVaJkSuBfrkvCMU5dwJ9r8BGA
+JhZTQuISMYd142XztXMlNzBmuZBuC9E4jQ==
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.csr.cnf b/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.csr.cnf
new file mode 100644
index 0000000..8ed5b2b
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.csr.cnf
@@ -0,0 +1,10 @@
+[ req ]
+prompt = no
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+O = ACME
+
+# shall be aligned with SGP.23 value #TEST_DP_ADDRESS2
+CN = testsmdpplus2.example.com
+
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.der b/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.der
new file mode 100644
index 0000000..be20b34
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.ext.cnf b/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.ext.cnf
new file mode 100644
index 0000000..d224daf
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP2_TLS.ext.cnf
@@ -0,0 +1,14 @@
+######################################################################################################################################################################
+# Extensions for a DPTLS
+keyUsage = critical, digitalSignature
+extendedKeyUsage = critical, serverAuth, clientAuth
+certificatePolicies = 2.23.146.1.2.1.3
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+
+# RID shall be aligend with SGP.23 value SM-DP+OID2
+# DNS name shall be aligned with SGP.23 value #TEST_DP_ADDRESS2
+subjectAltName = DNS:testsmdpplus2.example.com, RID:2.999.12
+
+crlDistributionPoints=URI:http://ci.test.example.com/CRL-A.crl, URI:http://ci.test.example.com/CRL-B.crl
+
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.csr.cnf b/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.csr.cnf
new file mode 100644
index 0000000..abaa1da
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.csr.cnf
@@ -0,0 +1,10 @@
+[ req ]
+prompt = no
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+O = ACME
+
+# shall be aligned with SGP.23 value #TEST_DP_ADDRESS4
+CN = testsmdpplus4.example.com
+
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.der b/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.der
new file mode 100644
index 0000000..07b4c85
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.ext.cnf b/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.ext.cnf
new file mode 100644
index 0000000..31f6463
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP4_TLS.ext.cnf
@@ -0,0 +1,14 @@
+######################################################################################################################################################################
+# Extensions for a DPTLS
+keyUsage = critical, digitalSignature
+extendedKeyUsage = critical, serverAuth, clientAuth
+certificatePolicies = 2.23.146.1.2.1.3
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+
+# RID shall be aligend with SGP.23 value SM-DP+OID4
+# DNS name shall be aligned with SGP.23 value #TEST_DP_ADDRESS4
+subjectAltName = DNS:testsmdpplus4.example.com, RID:2.999.14
+
+crlDistributionPoints=URI:http://ci.test.example.com/CRL-A.crl, URI:http://ci.test.example.com/CRL-B.crl
+
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.csr.cnf b/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.csr.cnf
new file mode 100644
index 0000000..39ebdfb
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.csr.cnf
@@ -0,0 +1,10 @@
+[ req ]
+prompt = no
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+O = ACME
+
+# shall be aligned with SGP.23 value #TEST_DP_ADDRESS8
+CN = testsmdpplus8.example.com
+
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.der b/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.der
new file mode 100644
index 0000000..3b45b14
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.ext.cnf b/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.ext.cnf
new file mode 100644
index 0000000..cc88f83
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP8_TLS.ext.cnf
@@ -0,0 +1,14 @@
+######################################################################################################################################################################
+# Extensions for a DPTLS
+keyUsage = critical, digitalSignature
+extendedKeyUsage = critical, serverAuth, clientAuth
+certificatePolicies = 2.23.146.1.2.1.3
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+
+# RID shall be aligend with SGP.23 value SM-DP+OID8
+# DNS name shall be aligned with SGP.23 value #TEST_DP_ADDRESS8
+subjectAltName = DNS:testsmdpplus8.example.com, RID:2.999.18
+
+crlDistributionPoints=URI:http://ci.test.example.com/CRL-A.crl, URI:http://ci.test.example.com/CRL-B.crl
+
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS.csr.cnf b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS.csr.cnf
new file mode 100644
index 0000000..d9b785c
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS.csr.cnf
@@ -0,0 +1,10 @@
+[ req ]
+prompt = no
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+O = ACME
+
+# shall be aligned with SGP.23 value #TEST_DP_ADDRESS1
+CN = testsmdpplus1.example.com
+
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS.ext.cnf b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS.ext.cnf
new file mode 100644
index 0000000..f80caf6
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS.ext.cnf
@@ -0,0 +1,14 @@
+######################################################################################################################################################################
+# Extensions for a DPTLS
+keyUsage = critical, digitalSignature
+extendedKeyUsage = critical, serverAuth, clientAuth
+certificatePolicies = 2.23.146.1.2.1.3
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+
+# RID shall be aligend with SGP.23 value SM-DP+OID
+# DNS name shall be aligned with SGP.23 value #TEST_DP_ADDRESS1
+subjectAltName = DNS:testsmdpplus1.example.com, RID:2.999.10
+
+crlDistributionPoints=URI:http://ci.test.example.com/CRL-A.crl, URI:http://ci.test.example.com/CRL-B.crl
+
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_BRP.der b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_BRP.der
new file mode 100644
index 0000000..dc730f0
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.der b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.der
new file mode 100644
index 0000000..0cb9ba7
--- /dev/null
+++ b/smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP2_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP2_TLS.der
new file mode 100644
index 0000000..4f91532
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP2_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP4_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP4_TLS.der
new file mode 100644
index 0000000..70820fa
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP4_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP8_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP8_TLS.der
new file mode 100644
index 0000000..33cc6fb
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP8_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_BRP.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_BRP.der
new file mode 100644
index 0000000..5d0ae30
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_NIST.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_NIST.der
new file mode 100644
index 0000000..38927b1
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2021/CERT_S_SM_DP_TLS_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP2_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP2_TLS.der
new file mode 100644
index 0000000..32909ce
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP2_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP4_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP4_TLS.der
new file mode 100644
index 0000000..ea11075
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP4_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP8_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP8_TLS.der
new file mode 100644
index 0000000..93a0cc0
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP8_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_BRP.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_BRP.der
new file mode 100644
index 0000000..3d00317
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_NIST.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_NIST.der
new file mode 100644
index 0000000..179c37b
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2022/CERT_S_SM_DP_TLS_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP2_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP2_TLS.der
new file mode 100644
index 0000000..da5516c
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP2_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP4_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP4_TLS.der
new file mode 100644
index 0000000..b1c222c
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP4_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP8_TLS.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP8_TLS.der
new file mode 100644
index 0000000..638e4a1
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP8_TLS.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_BRP.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_BRP.der
new file mode 100644
index 0000000..6746cbb
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_BRP.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_NIST.der b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_NIST.der
new file mode 100644
index 0000000..6977bd3
--- /dev/null
+++ b/smdpp-data/certs/DPtls/Old_TLS_Validity/Expired 2023/CERT_S_SM_DP_TLS_NIST.der
Binary files differ
diff --git a/smdpp-data/certs/DPtls/PK_S_SM_DP2_TLS_NIST.pem b/smdpp-data/certs/DPtls/PK_S_SM_DP2_TLS_NIST.pem
new file mode 100644
index 0000000..702bcf7
--- /dev/null
+++ b/smdpp-data/certs/DPtls/PK_S_SM_DP2_TLS_NIST.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGtkADHVON2E+dQ+3v3NC/tULwHJS
+7YRla0XW9wh4lQip/+CWFyOvEe/I3aaaafK1GIuzEhOCCrLdHvWALo/Utg==
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPtls/PK_S_SM_DP4_TLS.pem b/smdpp-data/certs/DPtls/PK_S_SM_DP4_TLS.pem
new file mode 100644
index 0000000..9c165c7
--- /dev/null
+++ b/smdpp-data/certs/DPtls/PK_S_SM_DP4_TLS.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOtvoTSOoGvY7otiZg2y7aKdiVrMN
+cCYwPcNrwV32ETbE++cCAbTFpm49rGsrO5V6mbtOdBAD6L90LwgTAqp9pg==
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPtls/PK_S_SM_DP8_TLS.pem b/smdpp-data/certs/DPtls/PK_S_SM_DP8_TLS.pem
new file mode 100644
index 0000000..0ff4586
--- /dev/null
+++ b/smdpp-data/certs/DPtls/PK_S_SM_DP8_TLS.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzkExGPpkmW7Yspv8rIUzg+ZdPyKh
+meeWa/QGmmlYOSvlOdAA6lGlSIsQh9brR8ABhG0eaH7EkOgagJZJQgxSaw==
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPtls/PK_S_SM_DP_TLS_BRP.pem b/smdpp-data/certs/DPtls/PK_S_SM_DP_TLS_BRP.pem
new file mode 100644
index 0000000..8fd826f
--- /dev/null
+++ b/smdpp-data/certs/DPtls/PK_S_SM_DP_TLS_BRP.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABEwizNgsjQIh+dhUO3LhB7zJ/ZBU
+1mx1wOt0p73nMOdhjvZbJwteguQ6eW+N7guvivvrilNiU3oC/WXHnkEZa7U=
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPtls/PK_S_SM_DP_TLS_NIST.pem b/smdpp-data/certs/DPtls/PK_S_SM_DP_TLS_NIST.pem
new file mode 100644
index 0000000..15ea8ea
--- /dev/null
+++ b/smdpp-data/certs/DPtls/PK_S_SM_DP_TLS_NIST.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKCQwdc6O/R+uZ2g5QH2ybkzLQ3CU
+YhybOWEz8bJLtQG4/k6yTT4NOS8lP28blGJws8opLjTbb3qHs6X2rJRfCA==
+-----END PUBLIC KEY-----
diff --git a/smdpp-data/certs/DPtls/SK_S_SM_DP2_TLS_NIST.pem b/smdpp-data/certs/DPtls/SK_S_SM_DP2_TLS_NIST.pem
new file mode 100644
index 0000000..b25606d
--- /dev/null
+++ b/smdpp-data/certs/DPtls/SK_S_SM_DP2_TLS_NIST.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIE5lYcZAiPZpkHrb45SxGoQkLgM6gqiEAjFjbckbTuP1oAoGCCqGSM49
+AwEHoUQDQgAEGtkADHVON2E+dQ+3v3NC/tULwHJS7YRla0XW9wh4lQip/+CWFyOv
+Ee/I3aaaafK1GIuzEhOCCrLdHvWALo/Utg==
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPtls/SK_S_SM_DP4_TLS.pem b/smdpp-data/certs/DPtls/SK_S_SM_DP4_TLS.pem
new file mode 100644
index 0000000..32bdfde
--- /dev/null
+++ b/smdpp-data/certs/DPtls/SK_S_SM_DP4_TLS.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIPJlnS9Sj0sRN0DVig0q8+srSOEiwrYKavb8lq2Gvm+koAoGCCqGSM49
+AwEHoUQDQgAEOtvoTSOoGvY7otiZg2y7aKdiVrMNcCYwPcNrwV32ETbE++cCAbTF
+pm49rGsrO5V6mbtOdBAD6L90LwgTAqp9pg==
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPtls/SK_S_SM_DP8_TLS.pem b/smdpp-data/certs/DPtls/SK_S_SM_DP8_TLS.pem
new file mode 100644
index 0000000..51ca168
--- /dev/null
+++ b/smdpp-data/certs/DPtls/SK_S_SM_DP8_TLS.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIP9uSlCbrds4EIgxwjzMLUQwevKB6SWWf4zfHZVUoCiNoAoGCCqGSM49
+AwEHoUQDQgAEzkExGPpkmW7Yspv8rIUzg+ZdPyKhmeeWa/QGmmlYOSvlOdAA6lGl
+SIsQh9brR8ABhG0eaH7EkOgagJZJQgxSaw==
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_BRP.pem b/smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_BRP.pem
new file mode 100644
index 0000000..6ad37b0
--- /dev/null
+++ b/smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_BRP.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BgkrJAMDAggBAQc=
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHgCAQEEID9nFSgCs/TH+uZ5WFX2glQeReNe//TooFVloPGRKnguoAsGCSskAwMC
+CAEBB6FEA0IABEwizNgsjQIh+dhUO3LhB7zJ/ZBU1mx1wOt0p73nMOdhjvZbJwte
+guQ6eW+N7guvivvrilNiU3oC/WXHnkEZa7U=
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_NIST.pem b/smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_NIST.pem
new file mode 100644
index 0000000..c1329f9
--- /dev/null
+++ b/smdpp-data/certs/DPtls/SK_S_SM_DP_TLS_NIST.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIKA+fORVBHS+pLeoc5nOWoyfZhtoD5QBOf/4Tp3sak2MoAoGCCqGSM49
+AwEHoUQDQgAEKCQwdc6O/R+uZ2g5QH2ybkzLQ3CUYhybOWEz8bJLtQG4/k6yTT4N
+OS8lP28blGJws8opLjTbb3qHs6X2rJRfCA==
+-----END EC PRIVATE KEY-----
diff --git a/smdpp-data/certs/README b/smdpp-data/certs/README
new file mode 100644
index 0000000..a3cd547
--- /dev/null
+++ b/smdpp-data/certs/README
@@ -0,0 +1 @@
+files taken from SGP.26 v3
diff --git a/smdpp-data/upp/TS48V1-A-UNIQUE.der b/smdpp-data/upp/TS48V1-A-UNIQUE.der
new file mode 100644
index 0000000..18559be
--- /dev/null
+++ b/smdpp-data/upp/TS48V1-A-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V1-B-UNIQUE.der b/smdpp-data/upp/TS48V1-B-UNIQUE.der
new file mode 100644
index 0000000..24ffb8d
--- /dev/null
+++ b/smdpp-data/upp/TS48V1-B-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V2-SAIP2-1-BERTLV-UNIQUE.der b/smdpp-data/upp/TS48V2-SAIP2-1-BERTLV-UNIQUE.der
new file mode 100644
index 0000000..657a48e
--- /dev/null
+++ b/smdpp-data/upp/TS48V2-SAIP2-1-BERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V2-SAIP2-1-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V2-SAIP2-1-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..3eba990
--- /dev/null
+++ b/smdpp-data/upp/TS48V2-SAIP2-1-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V2-SAIP2-3-BERTLV-UNIQUE.der b/smdpp-data/upp/TS48V2-SAIP2-3-BERTLV-UNIQUE.der
new file mode 100644
index 0000000..4c8d7f2
--- /dev/null
+++ b/smdpp-data/upp/TS48V2-SAIP2-3-BERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V2-SAIP2-3-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V2-SAIP2-3-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..52999e2
--- /dev/null
+++ b/smdpp-data/upp/TS48V2-SAIP2-3-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V3-SAIP2-1-BERTLV-UNIQUE.der b/smdpp-data/upp/TS48V3-SAIP2-1-BERTLV-UNIQUE.der
new file mode 100644
index 0000000..700edb5
--- /dev/null
+++ b/smdpp-data/upp/TS48V3-SAIP2-1-BERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V3-SAIP2-1-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V3-SAIP2-1-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..ab731ad
--- /dev/null
+++ b/smdpp-data/upp/TS48V3-SAIP2-1-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V3-SAIP2-3-BERTLV-UNIQUE.der b/smdpp-data/upp/TS48V3-SAIP2-3-BERTLV-UNIQUE.der
new file mode 100644
index 0000000..e340003
--- /dev/null
+++ b/smdpp-data/upp/TS48V3-SAIP2-3-BERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V3-SAIP2-3-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V3-SAIP2-3-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..59bfe56
--- /dev/null
+++ b/smdpp-data/upp/TS48V3-SAIP2-3-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V4-SAIP2-1A-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V4-SAIP2-1A-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..e8ac2c2
--- /dev/null
+++ b/smdpp-data/upp/TS48V4-SAIP2-1A-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V4-SAIP2-1B-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V4-SAIP2-1B-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..8cd3a31
--- /dev/null
+++ b/smdpp-data/upp/TS48V4-SAIP2-1B-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V4-SAIP2-3-BERTLV-UNIQUE.der b/smdpp-data/upp/TS48V4-SAIP2-3-BERTLV-UNIQUE.der
new file mode 100644
index 0000000..4c8433a
--- /dev/null
+++ b/smdpp-data/upp/TS48V4-SAIP2-3-BERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V4-SAIP2-3-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V4-SAIP2-3-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..8a08988
--- /dev/null
+++ b/smdpp-data/upp/TS48V4-SAIP2-3-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V5-SAIP2-1A-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V5-SAIP2-1A-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..2a91ef6
--- /dev/null
+++ b/smdpp-data/upp/TS48V5-SAIP2-1A-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V5-SAIP2-1B-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V5-SAIP2-1B-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..747cead
--- /dev/null
+++ b/smdpp-data/upp/TS48V5-SAIP2-1B-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V5-SAIP2-3-BERTLV-SUCI-UNIQUE.der b/smdpp-data/upp/TS48V5-SAIP2-3-BERTLV-SUCI-UNIQUE.der
new file mode 100644
index 0000000..d7cbeeb
--- /dev/null
+++ b/smdpp-data/upp/TS48V5-SAIP2-3-BERTLV-SUCI-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48V5-SAIP2-3-NOBERTLV-UNIQUE.der b/smdpp-data/upp/TS48V5-SAIP2-3-NOBERTLV-UNIQUE.der
new file mode 100644
index 0000000..0276eac
--- /dev/null
+++ b/smdpp-data/upp/TS48V5-SAIP2-3-NOBERTLV-UNIQUE.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v1_A.der b/smdpp-data/upp/TS48v1_A.der
new file mode 100644
index 0000000..e1dd1bd
--- /dev/null
+++ b/smdpp-data/upp/TS48v1_A.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v1_B.der b/smdpp-data/upp/TS48v1_B.der
new file mode 100644
index 0000000..8e17a15
--- /dev/null
+++ b/smdpp-data/upp/TS48v1_B.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v2_SAIP2.1_BERTLV.der b/smdpp-data/upp/TS48v2_SAIP2.1_BERTLV.der
new file mode 100644
index 0000000..bf85d76
--- /dev/null
+++ b/smdpp-data/upp/TS48v2_SAIP2.1_BERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v2_SAIP2.1_NoBERTLV.der b/smdpp-data/upp/TS48v2_SAIP2.1_NoBERTLV.der
new file mode 100644
index 0000000..7800677
--- /dev/null
+++ b/smdpp-data/upp/TS48v2_SAIP2.1_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v2_SAIP2.3_BERTLV.der b/smdpp-data/upp/TS48v2_SAIP2.3_BERTLV.der
new file mode 100644
index 0000000..802944c
--- /dev/null
+++ b/smdpp-data/upp/TS48v2_SAIP2.3_BERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v2_SAIP2.3_NoBERTLV.der b/smdpp-data/upp/TS48v2_SAIP2.3_NoBERTLV.der
new file mode 100644
index 0000000..0a0bdff
--- /dev/null
+++ b/smdpp-data/upp/TS48v2_SAIP2.3_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v3_SAIP2.1_BERTLV.der b/smdpp-data/upp/TS48v3_SAIP2.1_BERTLV.der
new file mode 100644
index 0000000..b2dcf71
--- /dev/null
+++ b/smdpp-data/upp/TS48v3_SAIP2.1_BERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v3_SAIP2.1_NoBERTLV.der b/smdpp-data/upp/TS48v3_SAIP2.1_NoBERTLV.der
new file mode 100644
index 0000000..6526fe9
--- /dev/null
+++ b/smdpp-data/upp/TS48v3_SAIP2.1_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v3_SAIP2.3_BERTLV.der b/smdpp-data/upp/TS48v3_SAIP2.3_BERTLV.der
new file mode 100644
index 0000000..4e07d61
--- /dev/null
+++ b/smdpp-data/upp/TS48v3_SAIP2.3_BERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v3_SAIP2.3_NoBERTLV.der b/smdpp-data/upp/TS48v3_SAIP2.3_NoBERTLV.der
new file mode 100644
index 0000000..e2d7835
--- /dev/null
+++ b/smdpp-data/upp/TS48v3_SAIP2.3_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v4_SAIP2.1A_NoBERTLV.der b/smdpp-data/upp/TS48v4_SAIP2.1A_NoBERTLV.der
new file mode 100644
index 0000000..edca305
--- /dev/null
+++ b/smdpp-data/upp/TS48v4_SAIP2.1A_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v4_SAIP2.1B_NoBERTLV.der b/smdpp-data/upp/TS48v4_SAIP2.1B_NoBERTLV.der
new file mode 100644
index 0000000..baac777
--- /dev/null
+++ b/smdpp-data/upp/TS48v4_SAIP2.1B_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v4_SAIP2.3_BERTLV.der b/smdpp-data/upp/TS48v4_SAIP2.3_BERTLV.der
new file mode 100644
index 0000000..8193d65
--- /dev/null
+++ b/smdpp-data/upp/TS48v4_SAIP2.3_BERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v4_SAIP2.3_NoBERTLV.der b/smdpp-data/upp/TS48v4_SAIP2.3_NoBERTLV.der
new file mode 100644
index 0000000..ae75009
--- /dev/null
+++ b/smdpp-data/upp/TS48v4_SAIP2.3_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v5_SAIP2.1A_NoBERTLV.der b/smdpp-data/upp/TS48v5_SAIP2.1A_NoBERTLV.der
new file mode 100644
index 0000000..0a6d251
--- /dev/null
+++ b/smdpp-data/upp/TS48v5_SAIP2.1A_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v5_SAIP2.1B_NoBERTLV.der b/smdpp-data/upp/TS48v5_SAIP2.1B_NoBERTLV.der
new file mode 100644
index 0000000..6dded61
--- /dev/null
+++ b/smdpp-data/upp/TS48v5_SAIP2.1B_NoBERTLV.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v5_SAIP2.3_BERTLV_SUCI.der b/smdpp-data/upp/TS48v5_SAIP2.3_BERTLV_SUCI.der
new file mode 100644
index 0000000..6c924c2
--- /dev/null
+++ b/smdpp-data/upp/TS48v5_SAIP2.3_BERTLV_SUCI.der
Binary files differ
diff --git a/smdpp-data/upp/TS48v5_SAIP2.3_NoBERTLV.der b/smdpp-data/upp/TS48v5_SAIP2.3_NoBERTLV.der
new file mode 100644
index 0000000..becd4eb
--- /dev/null
+++ b/smdpp-data/upp/TS48v5_SAIP2.3_NoBERTLV.der
Binary files differ
diff --git a/smpp2sim.py b/smpp2sim.py
index 65f6307..af5e1f5 100755
--- a/smpp2sim.py
+++ b/smpp2sim.py
@@ -33,9 +33,9 @@ from smpp.pdu import pdu_types, operations, pdu_encoding
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
-from pySim.transport import LinkBase, ProactiveHandler, argparse_add_reader_args, init_reader
+from pySim.transport import LinkBase, ProactiveHandler, argparse_add_reader_args, init_reader, ApduTracer
from pySim.commands import SimCardCommands
-from pySim.cards import UsimCard
+from pySim.cards import UiccCardBase
from pySim.exceptions import *
from pySim.cat import ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload
from pySim.cat import DeviceIdentities, Address
@@ -51,9 +51,14 @@ ESME_MSISDN='12'
# or actually route based on MSISDNs
hackish_global_smpp = None
+class MyApduTracer(ApduTracer):
+ def trace_response(self, cmd, sw, resp):
+ print("-> %s %s" % (cmd[:10], cmd[10:]))
+ print("<- %s: %s" % (sw, resp))
+
class Proact(ProactiveHandler):
- def __init__(self, smpp_factory):
- self.smpp_factory = smpp_factory
+ #def __init__(self, smpp_factory):
+ # self.smpp_factory = smpp_factory
@staticmethod
def _find_first_element_of_type(instlist, cls):
@@ -66,6 +71,7 @@ class Proact(ProactiveHandler):
proactive command from the SIM."""
def handle_SendShortMessage(self, data):
"""Card requests sending a SMS."""
+ print("SendShortMessage")
pp(data)
# Relevant parts in data: Address, SMS_TPDU
addr_ie = Proact._find_first_element_of_type(data.children, Address)
@@ -75,21 +81,26 @@ class Proact(ProactiveHandler):
self.send_sms_via_smpp(data)
def handle_OpenChannel(self, data):
"""Card requests opening a new channel via a UDP/TCP socket."""
+ print("OpenChannel")
pp(data)
pass
def handle_CloseChannel(self, data):
+ print("CloseChannel")
"""Close a channel."""
pp(data)
pass
def handleReceiveData(self, data):
+ print("ReceiveData")
"""Receive/read data from the socket."""
pp(data)
pass
def handleSendData(self, data):
+ print("SendData")
"""Send/write data to the socket."""
pp(data)
pass
def getChannelStatus(self, data):
+ print("GetChannelStatus")
pp(data)
pass
@@ -150,8 +161,8 @@ class MyServer:
def connect_to_card(self, tp: LinkBase):
self.tp = tp
self.scc = SimCardCommands(self.tp)
- self.card = UsimCard(self.scc)
- # this should be part of UsimCard, but FairewavesSIM breaks with that :/
+ self.card = UiccCardBase(self.scc)
+ # this should be part of UiccCardBase, but FairewavesSIM breaks with that :/
self.scc.cla_byte = "00"
self.scc.sel_ctrl = "0004"
self.card.read_aids()
@@ -181,21 +192,24 @@ class MyServer:
return pdu_types.CommandStatus.ESME_RINVDCS
# 1) build a SMS-DELIVER (!) from the SMPP-SUBMIT
- tpdu = SMS_DELIVER.fromSmppSubmit(pdu)
+ tpdu = SMS_DELIVER.from_smpp_submit(pdu)
print(tpdu)
# 2) wrap into the CAT ENVELOPE for SMS-PP-Download
- tpdu_ie = SMS_TPDU(decoded={'tpdu': b2h(tpdu.toBytes())})
+ tpdu_ie = SMS_TPDU(decoded={'tpdu': b2h(tpdu.to_bytes())})
+ addr_ie = Address(decoded={'ton_npi': 0x00, 'call_number': '0123456'})
dev_ids = DeviceIdentities(decoded={'source_dev_id': 'network', 'dest_dev_id': 'uicc'})
- sms_dl = SMSPPDownload(children=[dev_ids, tpdu_ie])
+ # TODO: Address is mandatory!
+ sms_dl = SMSPPDownload(children=[dev_ids, addr_ie, tpdu_ie])
+ #sms_dl = SMSPPDownload(children=[dev_ids, tpdu_ie])
# 3) send to the card
envelope_hex = b2h(sms_dl.to_tlv())
print("ENVELOPE: %s" % envelope_hex)
(data, sw) = self.scc.envelope(envelope_hex)
print("SW %s: %s" % (sw, data))
- if sw == '9300':
+ if sw in ['9200', '9300']:
# TODO send back RP-ERROR message with TP-FCS == 'SIM Application Toolkit Busy'
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
- elif sw == '9000' or sw[0:2] in ['6f', '62', '63']:
+ elif sw == '9000' or sw[0:2] in ['6f', '62', '63'] and len(data):
# data something like 027100000e0ab000110000000000000001612f or
# 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
# which is the user-data portion of the SMS starting with the UDH (027100)
@@ -233,11 +247,12 @@ if __name__ == '__main__':
opts = option_parser.parse_args()
- #tp = init_reader(opts, proactive_handler = Proact())
- tp = init_reader(opts)
+ tp = init_reader(opts, proactive_handler = Proact())
+ #tp = init_reader(opts)
if tp is None:
exit(1)
tp.connect()
+ tp.apdu_tracer = MyApduTracer()
ms = MyServer(opts.smpp_bind_port, opts.smpp_bind_ip)
ms.connect_to_card(tp)
diff --git a/smpp_ota_apdu2.py b/smpp_ota_apdu2.py
new file mode 100755
index 0000000..a90bc66
--- /dev/null
+++ b/smpp_ota_apdu2.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+import logging
+import sys
+from pprint import pprint as pp
+
+from pySim.ota import OtaKeyset, OtaDialectSms
+from pySim.utils import b2h, h2b
+
+import smpplib.gsm
+import smpplib.client
+import smpplib.consts
+
+logger = logging.getLogger(__name__)
+
+# if you want to know what's happening
+logging.basicConfig(level='DEBUG')
+
+class Foo:
+ def smpp_rx_handler(self, pdu):
+ sys.stdout.write('delivered {}\n'.format(pdu.receipted_message_id))
+ if pdu.short_message:
+ dec = self.ota_dialect.decode_resp(self.ota_keyset, self.spi, pdu.short_message)
+ pp(dec)
+ return None
+
+ def __init__(self):
+ # Two parts, UCS2, SMS with UDH
+ #parts, encoding_flag, msg_type_flag = smpplib.gsm.make_parts(u'Привет мир!\n'*10)
+
+ client = smpplib.client.Client('localhost', 2775, allow_unknown_opt_params=True)
+
+ # Print when obtain message_id
+ client.set_message_sent_handler(
+ lambda pdu: sys.stdout.write('sent {} {}\n'.format(pdu.sequence, pdu.message_id)))
+ #client.set_message_received_handler(
+ # lambda pdu: sys.stdout.write('delivered {}\n'.format(pdu.receipted_message_id)))
+ client.set_message_received_handler(self.smpp_rx_handler)
+
+ client.connect()
+ client.bind_transceiver(system_id='test', password='test')
+
+ self.client = client
+
+ if True:
+ KIC1 = h2b('000102030405060708090a0b0c0d0e0f')
+ KID1 = h2b('101112131415161718191a1b1c1d1e1f')
+ self.ota_keyset = OtaKeyset(algo_crypt='aes_cbc', kic_idx=1, kic=KIC1,
+ algo_auth='aes_cmac', kid_idx=1, kid=KID1)
+ self.tar = h2b('000001') # ISD-R according to Annex H of SGP.02
+ #self.tar = h2b('000002') # ECASD according to Annex H of SGP.02
+
+ if False:
+ KIC1 = h2b('4BE2D58A1FA7233DD723B3C70996E6E6')
+ KID1 = h2b('4a664208eba091d32c4ecbc299da1f34')
+ self.ota_keyset = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=1, kic=KIC1,
+ algo_auth='triple_des_cbc2', kid_idx=1, kid=KID1)
+ #KIC3 = h2b('A4074D8E1FE69B484A7E62682ED09B51')
+ #KID3 = h2b('41FF1033910112DB4EBEBB7807F939CD')
+ #self.ota_keyset = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=3, kic=KIC3,
+ # algo_auth='triple_des_cbc2', kid_idx=3, kid=KID3)
+ #self.tar = h2b('B00011') # USIM RFM
+ self.tar = h2b('000000') # RAM
+
+ if False: # sysmoEUICC1-C2G
+ KIC1 = h2b('B52F9C5938D1C19ED73E1AE772937FD7')
+ KID1 = h2b('3BC696ACD1EEC95A6624F7330D22FC81')
+ self.ota_keyset = OtaKeyset(algo_crypt='aes_cbc', kic_idx=1, kic=KIC1,
+ algo_auth='aes_cmac', kid_idx=1, kid=KID1)
+ self.tar = h2b('000001') # ISD-R according to Annex H of SGP.02
+ #self.tar = h2b('000002') # ECASD according to Annex H of SGP.02
+
+ if False: # TS.48 profile
+ KIC1 = h2b('66778899aabbccdd1122334455eeff10')
+ KID1 = h2b('112233445566778899aabbccddeeff10')
+ self.ota_keyset = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=1, kic=KIC1,
+ algo_auth='triple_des_cbc2', kid_idx=1, kid=KID1)
+ self.tar = h2b('000000') # ISD-P according to FIXME
+
+ if False: # TS.48 profile AES
+ KIC1 = h2b('66778899aabbccdd1122334455eeff10')
+ KID1 = h2b('112233445566778899aabbccddeeff10')
+ self.ota_keyset = OtaKeyset(algo_crypt='aes_cbc', kic_idx=2, kic=KIC1,
+ algo_auth='aes_cmac', kid_idx=2, kid=KID1)
+ self.tar = h2b('000000') # ISD-P according to FIXME
+
+
+
+ self.ota_dialect = OtaDialectSms()
+ self.spi = {'counter':'no_counter', 'ciphering':True, 'rc_cc_ds': 'cc', 'por_in_submit':False,
+ 'por_shall_be_ciphered':True, 'por_rc_cc_ds': 'cc', 'por': 'por_required'}
+
+
+ def tx_sms_tpdu(self, tpdu: bytes):
+ self.client.send_message(
+ source_addr_ton=smpplib.consts.SMPP_TON_INTL,
+ #source_addr_npi=smpplib.consts.SMPP_NPI_ISDN,
+ # Make sure it is a byte string, not unicode:
+ source_addr='12',
+
+ dest_addr_ton=smpplib.consts.SMPP_TON_INTL,
+ #dest_addr_npi=smpplib.consts.SMPP_NPI_ISDN,
+ # Make sure thease two params are byte strings, not unicode:
+ destination_addr='23',
+ short_message=tpdu,
+
+ data_coding=smpplib.consts.SMPP_ENCODING_BINARY,
+ esm_class=smpplib.consts.SMPP_GSMFEAT_UDHI,
+ protocol_id=0x7f,
+ #registered_delivery=True,
+ )
+
+ def tx_c_apdu(self, apdu: bytes):
+ logger.info("C-APDU: %s" % b2h(apdu))
+ # translate to Secured OTA RFM
+ secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu)
+ # add user data header
+ tpdu = b'\x02\x70\x00' + secured
+ # send via SMPP
+ self.tx_sms_tpdu(tpdu)
+
+
+f = Foo()
+print("initialized")
+#f.tx_c_apdu(h2b('80a40400023f00'))
+#f.tx_c_apdu(h2b('80EC010100'))
+f.tx_c_apdu(h2b('80EC0101' + '0E' + '350103' + '390203e8' + '3e052101020304'))
+f.client.listen()
diff --git a/tests/pysim-test.sh b/tests/pySim-prog_test.sh
index 4004c18..28d9777 100755
--- a/tests/pysim-test.sh
+++ b/tests/pySim-prog_test.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-# Utility to verify the functionality of pysim-prog.py
+# Utility to verify the functionality of pySim-prog.py
#
# (C) 2018 by Sysmocom s.f.m.c. GmbH
# All Rights Reserved
@@ -25,10 +25,11 @@ PYSIM_READ=../pySim-read.py
TEMPFILE=temp.tmp
PYTHON=python3
+export PYSIM_INTEGRATION_TEST=1
set -e
-echo "pysim-test - a test program to test pysim-prog.py"
-echo "================================================="
+echo "pySim-prog_test - a test program to test pySim-prog.py"
+echo "======================================================"
# Generate a list of the cards we expect to see by checking which .ok files
# are present
diff --git a/tests/pySim-trace_test.sh b/tests/pySim-trace_test.sh
new file mode 100755
index 0000000..3e200c3
--- /dev/null
+++ b/tests/pySim-trace_test.sh
@@ -0,0 +1,96 @@
+#!/bin/bash
+
+# Utility to verify the functionality of pySim-trace.py
+#
+# (C) 2023 by Sysmocom s.f.m.c. GmbH
+# All Rights Reserved
+#
+# Author: Philipp Maier
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+PYSIM_TRACE=../pySim-trace.py
+GSMTAP_TRACE=pySim-trace_test_gsmtap.pcapng
+TEMPFILE=temp.tmp
+
+export PYSIM_INTEGRATION_TEST=1
+# to avoid termcolor.colored generating colors; https://github.com/termcolor/termcolor
+export ANSI_COLORS_DISABLED=1
+
+echo "pySim-trace_test - a test program to test pySim-trace.py"
+echo "========================================================"
+
+function usage {
+ echo "Options:"
+ echo "-o: generate .ok file"
+}
+
+function gen_ok_file {
+ $PYSIM_TRACE gsmtap-pyshark-pcap -f $GSMTAP_TRACE > $GSMTAP_TRACE.ok
+ echo "Generated file: $GSMTAP_TRACE.ok"
+ echo "------------8<------------"
+ cat $GSMTAP_TRACE.ok
+ echo "------------8<------------"
+}
+
+function run_test {
+ $PYSIM_TRACE gsmtap-pyshark-pcap -f $GSMTAP_TRACE | tee $TEMPFILE
+ if [ ${PIPESTATUS[0]} -ne 0 ]; then
+ echo ""
+ echo "========================================================"
+ echo "Testrun with $GSMTAP_TRACE failed (exception)."
+ rm -f $TEMPFILE
+ exit 1
+ fi
+
+ DIFF=`diff $GSMTAP_TRACE.ok $TEMPFILE`
+ if ! [ -z "$DIFF" ]; then
+ echo "Testrun with $GSMTAP_TRACE failed (unexpected output)."
+ echo "------------8<------------"
+ diff $GSMTAP_TRACE.ok $TEMPFILE
+ echo "------------8<------------"
+ rm -f $TEMPFILE
+ exit 1
+ fi
+
+ echo ""
+ echo "========================================================"
+ echo "trace parsed without problems -- everything ok!"
+ rm -f $TEMPFILE
+}
+
+OPT_GEN_OK_FILE=0
+while getopts ":ho" OPT; do
+ case $OPT in
+ h)
+ usage
+ exit 0
+ ;;
+ o)
+ OPT_GEN_OK_FILE=1
+ ;;
+ \?)
+ echo "Invalid option: -$OPTARG" >&2
+ exit 1
+ ;;
+ esac
+done
+
+if [ $OPT_GEN_OK_FILE -eq 1 ]; then
+ gen_ok_file
+ exit 0
+else
+ run_test
+ exit 0
+fi
diff --git a/tests/test_construct.py b/tests/test_construct.py
index f1bee5a..6251a14 100644
--- a/tests/test_construct.py
+++ b/tests/test_construct.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
import unittest
+from pySim.utils import b2h, h2b
from pySim.construct import *
+from construct import FlagsEnum
tests = [
( b'\x80', 0x80 ),
@@ -33,5 +35,69 @@ class TestUtils(unittest.TestCase):
self.assertEqual(filter_dict(inp), out)
+class TestUcs2Adapter(unittest.TestCase):
+ # the three examples from TS 102 221 Annex A
+ EXAMPLE1 = b'\x80\x00\x30\x00\x31\x00\x32\x00\x33'
+ EXAMPLE2 = b'\x81\x05\x13\x53\x95\xa6\xa6\xff\xff'
+ EXAMPLE3 = b'\x82\x05\x05\x30\x2d\x82\xd3\x2d\x31'
+ ad = Ucs2Adapter(GreedyBytes)
+
+ def test_example1_decode(self):
+ dec = self.ad._decode(self.EXAMPLE1, None, None)
+ self.assertEqual(dec, "0123")
+
+ def test_example2_decode(self):
+ dec = self.ad._decode(self.EXAMPLE2, None, None)
+ self.assertEqual(dec, "S\u0995\u09a6\u09a6\u09ff")
+
+ def test_example3_decode(self):
+ dec = self.ad._decode(self.EXAMPLE3, None, None)
+ self.assertEqual(dec, "-\u0532\u0583-1")
+
+ testdata = [
+ # variant 2 with only GSM alphabet characters
+ ( "mahlzeit", '8108006d61686c7a656974' ),
+ # variant 2 with mixed GSM alphabet + UCS2
+ ( "mahlzeit\u099523", '810b136d61686c7a656974953233' ),
+ # variant 3 due to codepoint exceeding 8 bit
+ ( "mahl\u8023zeit", '820980236d61686c807a656974' ),
+ # variant 1 as there is no common codepoint pointer / prefix
+ ( "\u3000\u2000\u1000", '80300020001000' ),
+ ]
+
+ def test_data_decode(self):
+ for string, encoded_hex in self.testdata:
+ encoded = h2b(encoded_hex)
+ dec = self.ad._decode(encoded, None, None)
+ self.assertEqual(dec, string)
+
+ def test_data_encode(self):
+ for string, encoded_hex in self.testdata:
+ encoded = h2b(encoded_hex)
+ re_enc = self.ad._encode(string, None, None)
+ self.assertEqual(encoded, re_enc)
+
+class TestTrailerAdapter(unittest.TestCase):
+ Privileges = FlagsEnum(StripTrailerAdapter(GreedyBytes, 3), security_domain=0x800000,
+ dap_verification=0x400000,
+ delegated_management=0x200000, card_lock=0x100000,
+ card_terminate=0x080000, card_reset=0x040000,
+ cvm_management=0x020000, mandated_dap_verification=0x010000,
+ trusted_path=0x8000, authorized_management=0x4000,
+ token_management=0x2000, global_delete=0x1000,
+ global_lock=0x0800, global_registry=0x0400,
+ final_application=0x0200, global_service=0x0100,
+ receipt_generation=0x80, ciphered_load_file_data_block=0x40,
+ contactless_activation=0x20, contactless_self_activation=0x10)
+ examples = ['00', '80', '8040', '400010']
+ def test_examples(self):
+ for e in self.examples:
+ dec = self.Privileges.parse(h2b(e))
+ reenc = self.Privileges.build(dec)
+ self.assertEqual(e, b2h(reenc))
+
+
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_esim.py b/tests/test_esim.py
new file mode 100755
index 0000000..81926e5
--- /dev/null
+++ b/tests/test_esim.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+# (C) 2023 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import logging
+import base64
+
+from pySim.utils import b2h, h2b
+from pySim.esim.bsp import *
+import pySim.esim.rsp as rsp
+from pySim.esim import ActivationCode
+
+from cryptography.hazmat.primitives.asymmetric import ec
+
+class TestActivationCode(unittest.TestCase):
+ def test_de_encode(self):
+ STRS = ['1$SMDP.GSMA.COM$04386-AGYFT-A74Y8-3F815',
+ '1$SMDP.GSMA.COM$04386-AGYFT-A74Y8-3F815$$1',
+ '1$SMDP.GSMA.COM$04386-AGYFT-A74Y8-3F815$1.3.6.1.4.1.31746$1',
+ '1$SMDP.GSMA.COM$04386-AGYFT-A74Y8-3F815$1.3.6.1.4.1.31746',
+ '1$SMDP.GSMA.COM$$1.3.6.1.4.1.31746']
+ for s in STRS:
+ ac = ActivationCode.from_string(s)
+ self.assertEqual(s, ac.to_string())
+
+
+class TestECKA(unittest.TestCase):
+ def test_mode51(self):
+ curve = ec.SECP256R1()
+ euicc_otpk_der = h2b('0400f7b8d71403f21d84b00cd9e561178d737d3f4d065e62fee279271298dd4f074794ab791b9939d4461296efe388aa26731064263af988b7d2c4d77da44801b5')
+ smdp_otpk_der = h2b('04a27e2bdbd94dcf67d4c9ae5cb149d9d0f093be7a16dc41ec9db0318e4db72d09234a7d7631979a5d150eec40afe17ce41673df9d2f2e4246d60051c74eba7964')
+ smdp_otsk_bytes = h2b('fb68a38ccedb69e15cbe03c256228998ac398587e5dc7117f948145c839d61a4')
+ expected_shared_secret = h2b('c9a993dd4879a8f7161f2085410edd4f9652f1df37be097ba96ba2ca6be528fe')
+
+ euicc_otpk = ec.EllipticCurvePublicKey.from_encoded_point(curve, bytes(euicc_otpk_der))
+ smdp_otpk = ec.EllipticCurvePublicKey.from_encoded_point(curve, bytes(smdp_otpk_der))
+ smdp_otsk = ec.derive_private_key(int.from_bytes(smdp_otsk_bytes, 'big'), curve)
+
+ shared_secret = smdp_otsk.exchange(ec.ECDH(), euicc_otpk)
+
+ self.assertEqual(shared_secret, expected_shared_secret)
+
+
+class TestBSPdecode(unittest.TestCase):
+ bpp_b64 = "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"
+
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_esim_bsp.py b/tests/test_esim_bsp.py
new file mode 100755
index 0000000..d7b37b0
--- /dev/null
+++ b/tests/test_esim_bsp.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import logging
+import base64
+
+from pySim.utils import b2h, h2b
+from pySim.esim.bsp import *
+import pySim.esim.rsp as rsp
+
+class BSP_Test(unittest.TestCase):
+ shared_secret = h2b('8902dca391bbb22570fe60c176076246f568b1941265dff1d729e63039658089')
+ eid_bin = h2b('89049032123451234512345678901335')
+ def test_kdf(self):
+ s_enc, s_mac, icv = bsp_key_derivation(self.shared_secret, 0x88, 16, b'\x80'*8, self.eid_bin)
+ self.assertEqual(s_enc, h2b('d782d575b22a38334556a3d9a1e2ae6b'))
+ self.assertEqual(s_mac, h2b('a35addc192ae8f934e2932116c7e89e7'))
+ self.assertEqual(icv, h2b('734bc93ddb10c71c5ad13d420dc08b5b'))
+
+class BSP_Test_mode51(unittest.TestCase):
+ """This test was created using hex-dumps from a log/trace of a 3rd party SM-DP+. We therefore use these
+ test vectors to verify our implementation is in agreement with that other implementation."""
+ shared_secret = h2b('c9a993dd4879a8f7161f2085410edd4f9652f1df37be097ba96ba2ca6be528fe')
+ eid_bin = h2b('89049032123451234512345678901235')
+
+ def test_kdf(self):
+ s_enc, s_mac, icv = bsp_key_derivation(self.shared_secret, 0x88, 16, b'\x80'*8, self.eid_bin)
+ self.assertEqual(s_enc, h2b('472f5bfadd97f21d34f3ce9b51b92751'))
+ self.assertEqual(s_mac, h2b('9ec07f5b36a13e12a991d66e294e6242'))
+ self.assertEqual(icv, h2b('406d507b448a699e7a36a38494debbde'))
+
+ def test_ciphering(self):
+ bi = BspInstance(h2b('472f5bfadd97f21d34f3ce9b51b92751'), h2b('9ec07f5b36a13e12a991d66e294e6242'), h2b('406d507b448a699e7a36a38494debbde'))
+ output = bi.encrypt_and_mac(0x87, h2b('bf2400'))
+ self.assertEqual(output[0], h2b('8718f6fe031e4b9cfe87c8e1e62f3fde85c49412d7722a6a2d89'))
+
+ output = bi.mac_only(0x88, h2b('bf252d5a0a98001032547698103214910947534d415f54455354921147534d415f544553545f50524f46494c45950100'))
+ self.assertEqual(output[0], h2b('8838bf252d5a0a98001032547698103214910947534d415f54455354921147534d415f544553545f50524f46494c459501008a62cc9adbb5ccbc'))
+
+ output = bi.encrypt_and_mac(0x87, h2b('bf26368010000102030405060708090a0b0c0d0e0f8110010102030405060708090a0b0c0d0e0f8210020102030405060708090a0b0c0d0e0f'))
+ self.assertEqual(output[0], h2b('8748a14c800a001992351cd46ad2945654674369701d82b7b46567652bc8fbed234939fc57fba748015525fd6c651e9d3d1330652d42a0cfad950e912122af4ec5362d3c0bc535729c40'))
+
+ # new key material after replaceSessionKeys
+ bi = BspInstance(b'\x01\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f',
+ b'\x02\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f',
+ b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f')
+
+ segment0 = h2b('a048800102810101821a53494d616c6c69616e63652053616d706c652050726f66696c65830a89000123456789012341a506810084008b00a610060667810f010201060667810f010204b08201f8a0058000810101810667810f010201a207a105c60301020aa305a1038b010fa40c830a98001032547698103214a527a109820442210026800198831a61184f10a0000000871002ff33ff01890000010050045553494da682019ea10a8204422100258002022b831b8001019000800102a406830101950108800158a40683010a95010882010a8316800101a40683010195010880015aa40683010a95010882010f830b80015ba40683010a95010882011a830a800101900080015a970082011b8316800103a406830101950108800158a40683010a95010882010f8316800111a40683010195010880014aa40683010a95010882010f8321800103a406830101950108800158a40683010a950108840132a4068301019501088201048321800101a406830101950108800102a406830181950108800158a40683010a950108820104831b800101900080011aa406830101950108800140a40683010a95010882010a8310800101900080015aa40683010a95010882011583158001019000800118a40683010a95010880014297008201108310800101a40683010195010880015a97008201158316800113a406830101950108800148a40683010a95010882010f830b80015ea40683010a95010882011a83258001019000800102a010a406830101950108a406830102950108800158a40683010a950108a33fa0058000810102a13630118001018108303030303030303082020099300d800102810831323334353637383012800200818108313233343536373882020088a241a0058000810103a138a0363010800101810831323334ffffffff8201013010800102810830303030ffffffff820102301080010a810835363738ffffffff830101a182029ea0058000810104a18202933082028f62228202782183027ff18410a0000000871002ff33ff0189000001008b010ac60301810a62118202412183026f078b01028001098801388109082943019134876765621482044221002583026f068b010a8801b8c7022f06621a8202412183026f088b0105800121880140a507c00180c10207ff621a8202412183026f098b0105800121880148a507c00180c10207ff62168202412183026f318b0102800101880190a503c1010a62118202412183026f388b010280010e880120810d0a2e178ce73204000000000000621982044221001a83026f3b8b0108800202088800a504c10200ff62198204422100b083026f3c8b0105800206e08800a504c10200ff621282044221002683026f428b0105800126')
+ output = bi.encrypt_and_mac_one(0x86, segment0)
+ self.assertEqual(output, h2b('868203f80fd36b066b43e53906b33263c4141d18036fcac7bde47faed79e76514d39f1f405d9785ff04badf379a96bcf4685eb861239da34eeb213d0cd1e0d85e96e36097a52f600907e08b6f01232eb3792a00cc6b3288cc832c7f30357edfe0818d1f39ad8d55b5bd5d9c65917d0d16ee7e1421a44453714d66209bf441425bf6d153a85ba7e7406d58a4c46fb930bb14eefd6a853434619805429cbbb003bd5a56e7bceaf4666eb352ad46f47ea572c6ad311683803db3bec4d783583858f2ecfbaec3ed780fb3e3a645b1bf5cfd1047e5862c4a8877382dabd6aef6fa72ec6378ce7b21502d3267514b29bd589703ee7bd5b1e6d868b6b7c55006bc32406a924f44bfdb6c801d490450a54633bbd66bb0ece8b3a9be09433c537f3e3b2b8e45833365a94479b1895e9ba13387fca116565c267b6bc1274c82510b21ac9fa77574412351468fd1eac8928c9494e5eb8a909d9372476c62e8a2b556b557a79cdb47503ba5d9fd86d1962c23b3289f37df9a05668957df34c37806359af1d8dfecedf9bbf4888be9753f2b449ce4e5cde510572f9be4fb506f329a848c4583ac9ac710e53d512ad504f2e3769c2911c34f84ad0622c5428446d1e9c59bb6b2029f231b05ef45d3ec60fceff121ba684a023037b753f855e1067b8ad02783c04eb81ba47fcec0947bbc9661d90c6f00c0f4e6f9c22e0b25905379f8b265b436a4a74253a64c5734f956d134cf5b6c0671125b20b735f405fee1fc1941ed141734fa6856e898dc655fb91b045b3797d14b97a68ed3480ec137565d6c4d007f95ee705d452d0344ce9e9bbd0712f399ef8604f3f472b403ea64f00c969fd30a012cb8be049c54556994117fc5f6043930107774910be5f47c780427de063a56d45400a814c86dfcc2465262baf273a6ca89e77bb8c73efe2d22d975516b80c648404b10d3f329776a21251bedf445ade24b656e8635f0d7fe39772d942d9432766efb5152c6e990123c52744f1c402962a381ff2aaddaa1bd522a9b65a229f9adcd4099475b3f9ba45161ee35206365181e1d69d263d8d27444a7c3a2d7ddbb398ac5affea28cb0373057dad7741e52adb95822fee5157ce3c0ad978c5231a219ca0726d50eeb0c69d579094a54820194aeef13a365d85c52257f51cb65c567e0cdf1ce38b80eaa4d131ab7086e303c5728cf41a25df954d60ac12400d2a2990e67dbeff866736937beb8fce526be1dd5dc5d77d00b8783b34691a3e9bda7e697eec4cfa70b25914795af23ec6d258530f71402b9230947dc99f9140b16a54ba1291f54ef736de6711c1ae074b627dfc2fc5d5250f812b7ecd1d0f5020a3acf20ad5759cd9e761ce21882977f7db66aaba2d8f5190f6f23996af14b670ccac066c93af06a41f19d9ee712ce13fbe73b99e55401b208b991c8ced12d34c'))
+
+ segment1 = h2b('880062158202412183026f438b01058001028800a503c0018062128202412183026f468b036f060a8001118800810c0253494d616c6c69616e636562118202412183026f568b0108800101880128810100621b8202412183026f5b8b0105800106880178a508c00180c203f0000062168202412183026f5c8b0102800103880180a503c0018062168202412183026f738b010580010e880160a503c00180020107810700f1100000ff0162118202412183026f788b01028001028801308102004062118202412183026f7b8b010580010c88016862168202412183026f7e8b010580010b880158a503c0018002010781040000ff0162168202412183026fad8b010a800104880118a503c10100020103810102621382044221000483026fb78b010a800104880108810419f1ff0162158202412183026fc48b01058001808800a503c0018062168202412183026fe38b01058001128801f0a503c0018002010f810300000162168202412183026fe48b01058001508801c0a503c00180a225a0058000810105a11ca01a301880020081810831323334ffffffff82020081830101840122a43aa0058000810106a131a12f8001018101018210000102030405060708090a0b0c0d0e0f83100102030405060708090a0b0c0d0e0f008603010203a681bba0058000810107a1444f07a00000015153504f08a0000001515350414f08a000000151000000820382dc0083010fc90a810280008201f08701f0ea11800f0100000100000002011203b2010000a26c3022950138820101830101301730158001808610112233445566778899aabbccddeeff103022950134820102830101301730158001808610112233445566778899aabbccddeeff1030229501c8820103830101301730158001808610112233445566778899aabbccddeeff10a681c0a0058000810108a1494f07a00000015153504f08a0000001515350414f10a00000055910100102736456616c7565820380800083010fc907810280008201f0ea11800f01000001000000020112036c756500a26c30229501388201018301013017301580018086108811223344556677881122334455667730229501348201028301013017301580018086108811223344556677881122334455667730229501c882010383010130173015800180861088112233445566778811223344556677a8820263a0058000810109a18202184f08a000000559101001c482020a01002edecaffed020204000108a0000005591010011b636f6d2f67736d612f65756963632f746573742f6170706c657431020021002e0021000f003b002a00210066000a000e0000008a040f00000000000004010004003b04030107a0000000620101000110a0000000090005ffffffff')
+ output = bi.encrypt_and_mac_one(0x86, segment1)
+ self.assertEqual(output, h2b('868203f8648e034ae0dc4ce022ee1e60b130dda95e13b21b0da3de7677677f47900c1beb3637b8aa35f3a9e096c0285ffe3e931983df900b36b7e6bc4b9af14b0ee3d49637eb2d4cff314b5d00789a751dfd9554651fb2b7c66ad4e22a794d5b88cb71ccf4c05d53abeba8bd3b0c8209346f014cbdee62be4878e3fea09a96007135a6c584aa843c48972842bdbece1c439723021b3f0d535d557995beedbd2b56f416148df90cb1a4d2fa26288801d56a2cbb0a404f2fd9a73042d7a3486bfb7256c1d274aae5b7ec24e8eba28b7dce69edc44189b24186b98397b4a74831f8ab46e8e46a2ed3077d4924f5d3f6e4c1de5ddffd194e7f0f97d94ea2801d1364835c9871bae6539e3e1355c5970711d845864f04d9c1dccac0d4068dcf4664e9976509fe43fec6beb3ddc96839aba6d89bb1593c5b6ebbc32fff39c4a5bb3e5c9df6a1abc05818dbd5149733381e69521066e1bbd648eb19b00602767e90beaeb3b3b92679940a603c0500e37892d7b4fa44355c3deec8af207f89d04f83cd88603e9cb9c96f74643816e87af85a8a9d0283cdf535d1fbaefb930fd4a0dba2ae30ee9d2d9e2a31827a012a6380af42ac87f3bfd7079ddd8fa27d2299fb4d5879e9a17a5062e13cab4f7bed22ed54932fa53d630bca8592f957a7ed148e9d4f28075c2565a550694b876091a1181ba512e70fde4f28ae6968a18d721396c0fee9cd7744dee90bf85f5adddb3417b3ede9ea3cfd5eae2d820b17600ce3b95f6df38a5bc39302c5155c3f241ddfc7cee527af7f6a67868a577c39e76e26c4ed5d6aca031a97c280da27ae8de20e57a1dbab40a31e96e054f9a6f50578fd00156e37b70eead71af3258075c1ba84282aea462553504a868443b301ed99dcd5f414b720ef67cf5c4d16f1f7b9df741c2343246dcb717f3fa62b633539bdcc0082d161499caad8d097be78133dafd19777559f77c6d7a8f61323ff660613aa47cee26a4f7515204eee3c7eaa00eb55529b0ddae3436ec679fc591fe2063de94db00b5d0f041beeb80a91f108f7cf4b3b1344b0fbb437630ee437b4c7744c54009a59a9099681f3a3fa386f294c0eb4562581202a369772833efdc6e840695352de3864671e7fb0fd081ec162a2a62ea5b8a9da837f3920b4196fbe2ec912ade440537ae4a07dfc115c9c030539f278e0801bda4f15298ba50e329b18992af8b899686ec97175509d4a217d2eba8feef5f5732fc7be86370f7723896b784bd45517af86a7521e952b6be924d91a5190e3c2c65ce8924df43ddb25c529dde324a722a156df459f4b38bb062975fad9fadd27f6425e422b1abd9a7259f0bd712a486e1aa25ca848cf65c5fb888bf61ae136b68cf55cb643c198537cd83df0dbb842c5f4982ca3088cc2d8e867c3049a84b515ec39b0b774a8482099327006acff'))
+
+
+class TestBSPdecode(unittest.TestCase):
+ """This test verifies whether some fully encoded/encrypted/MACed bound profile package can be properly
+ decrypted and verified using our BSP code base."""
+ bpp_b64 = "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"
+
+ def test_decrypt_and_mac_verify(self):
+ bpp_bin = base64.b64decode(self.bpp_b64)
+ iscr = rsp.asn1.decode('BoundProfilePackage', bpp_bin)
+
+ # BSP segment for ConfigureISDP using S-ENC/S-MAC
+ bsp = BspInstance(h2b('f11b921c302efb1df6bf218bf2de0dd8'), h2b('8cd4d3a761eeac108641d5f4531a137e'), h2b('ee3140eafd692b68eff40119b363bc6c'))
+ plain_cfg_isdp = bsp.demac_and_decrypt(iscr['firstSequenceOf87'])
+ self.assertEqual(plain_cfg_isdp, h2b('bf2400'))
+
+ # BSP segment for StoreMetadata using /S-MAC
+ plain_smd = bsp.demac_only(iscr['sequenceOf88'])
+ self.assertEqual(plain_smd, h2b('bf25285a0a89000123456789012341910a4f736d6f636f6d53504e920e4f736d6f636f6d50726f66696c65'))
+
+ # BSP payload using PPK-ENC/PPK-MAC
+ bsp_ppk = BspInstance(h2b('00000000000000000000000000000000'), h2b('11111111111111111111111111111111'), h2b('22222222222222222222222222222222'))
+ plain_upp = bsp_ppk.demac_and_decrypt(iscr['sequenceOf86'])
+ original_upp = b'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wLSCCiugBYAAgQEJgQZngQ8BAgWiEaEPggJBIYMCbwWLA28GBaUAoxmhF4ICQSGDAm83iwNvBgmIAKUGwAFAwgEApB+hHYIERiEAA4MCbzmLA28GC4ABD4gB4KUGwAGAwgEApRqhFIICQSGDAm8+iwNvBgSAAQKIAKUAgwL//6YaoRSCAkEhgwJvP4sDbwYEgAECiAClAIMC//+nGKEWggRCIQAcgwJvQIsDbwYEgAE4iAClAKgaoRGCAkEhgwJvQYsDbwYBiAClAIMF////AACpFqEUggJBIYMCb0WLA28GAoABCIgApQCqFKESggJBIYMCb0iLA28GBIABCKUAqxihFoIEQiEAHIMCb0mLA28GCoABjIgApQCsHKEaggRCIQANgwJvS4sDbwYBgAFBiAClBMECAP+tHKEaggRCIQANgwJvTIsDbwYDgAFBiAClBMECAP+uFqEUggJBIYMCb1CLA28GBoABEIgApQCvgcGhEoICQSGDAm9giwNvBgaAAaqlAIOBqv///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAAsIIBEaESggJBIYMCb2GLA28GA4AB+qUAg4H6////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AALGCARGhEoICQSGDAm9iiwNvBgOAAfqlAIOB+v///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AAD///8AAP///wAA////AACyFqEUggJBIYMCbyyLA28GAogApQPAAUCzGaEXggJBIYMCbzKLA28GA4ABPIgApQPAAUC0HaEbggRCIQAegwJvR4sDbwYCgAIBLIgApQTBAgD/tRuhGYIEQiEAFYMCb02LA28GAYABP4gApQPAAUC2HKEaggRCIQANgwJvTosDbwYCgAFBiAClBMECAP+3E6ERggRCIQAPgwJvT4sDbwYCpQC5GqEYggJBIYMCb1eLA28GAYAByIgApQTBAgD/uhuhGYIEQiEAVYMCb1iLA28GA4ABqogApQPAAUC7RKFCggRGIQAqgwJvgIsDbwYCgAHSpS7AAYDBKf//////////////////////////////////////////////AAAAAAD/vEOhQYIERiEAKYMCb4GLA28GAoABzaUtwAGAwSj//////////////////////////////////////////////wAAAAD/vR6hHIIERiEAA4MCb4KLA28GC4ABHogApQbAAYDCAQC+HqEcggRGIQADgwJvg4sDbwYLgAEeiAClBsABgMIBAL8fgeShF4ICQSGDAm+xiwNvBgSAAciIAKUDwAFAg4HIIf///yHz//8hQ///IUP4/yFDGf8hUyD5IVPx/yFT8v8hU/P/IVP0/yFT9f8hU/b/IVP3/yFT+P8hU/n/AgDw/wIA8f8CAPL/AgDz/wIA9P8CAPX/AgD2/wIA9/8CAPj/AgD5/wIQ8P9mZvD/Zmbx/2Zm8v9mZoP/Zmb0/2Zm9f9mZvb/Zmb3/2Zm+P9mZvn/Znbw/wgh8P8IIfH/CCHy/wgh8/8IIfT/CCH1/wgh9v8IIff/CCH4/wgh+f8IMfD/mZn5/xEREfm/IB+hFIICQSGDAm+yiwNvBgOIAKUDwAFAgwcVAAAA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'
+ self.assertEqual(plain_upp, base64.b64decode(original_upp))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_esim_saip.py b/tests/test_esim_saip.py
new file mode 100755
index 0000000..a70c149
--- /dev/null
+++ b/tests/test_esim_saip.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import logging
+import copy
+
+from pySim.utils import h2b, b2h
+from pySim.esim.saip import *
+from pySim.esim.saip.personalization import *
+from pprint import pprint as pp
+
+
+class SaipTest(unittest.TestCase):
+ with open('smdpp-data/upp/TS48v2_SAIP2.3_NoBERTLV.der', 'rb') as f:
+ per_input = f.read()
+ pes = ProfileElementSequence.from_der(per_input)
+ expected_pet_list = ['header', 'mf', 'pukCodes', 'pinCodes', 'telecom', 'pinCodes', 'genericFileManagement', 'usim', 'opt-usim', 'pinCodes', 'akaParameter', 'gsm-access', 'df-5gs', 'df-saip','csim', 'opt-csim', 'pinCodes', 'cdmaParameter', 'isim', 'opt-isim', 'pinCodes', 'akaParameter', 'genericFileManagement', 'genericFileManagement', 'securityDomain', 'rfm', 'rfm', 'rfm', 'rfm', 'end']
+
+ def test_reencode_sequence(self):
+ """Test that we can decode and re-encode the entire DER encoded UPP."""
+ reencoded_der = self.pes.to_der()
+ self.assertEqual(reencoded_der, self.per_input)
+
+ def test_reencode_pe(self):
+ """Test that we can decode and re-encode reach individual ProfileElement."""
+ remainder = self.per_input
+ while len(remainder):
+ first_tlv, remainder = bertlv_first_segment(remainder)
+ pe = ProfileElement.from_der(first_tlv)
+ with self.subTest(pe.type):
+ reenc_tlv = pe.to_der()
+ self.assertEqual(reenc_tlv, first_tlv)
+
+
+ def test_sequence_helpers(self):
+ """Verify that the convenience helpers worked as expected."""
+ self.assertEqual([x.type for x in self.pes.pe_list], self.expected_pet_list)
+ self.assertEqual(len(self.pes.pes_by_naa), 4)
+
+ def test_personalization(self):
+ """Test some of the personalization operations."""
+ pes = copy.deepcopy(self.pes)
+ params = [Puk1('01234567'), Puk2(98765432), Pin1('1111'), Pin2(2222), Adm1('11111111'),
+ K(h2b('000102030405060708090a0b0c0d0e0f')), Opc(h2b('101112131415161718191a1b1c1d1e1f'))]
+ for p in params:
+ p.validate()
+ p.apply(pes)
+ # TODO: we don't actually test the results here, but we just verify there is no exception
+ pes.to_der()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_euicc.py b/tests/test_euicc.py
new file mode 100755
index 0000000..29c66ea
--- /dev/null
+++ b/tests/test_euicc.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+import unittest
+
+from pySim.euicc import *
+
+class TestEid(unittest.TestCase):
+
+ def test_eid_verify(self):
+ for eid in ['89049032123451234512345678901235', '89086030202200000022000023022943',
+ '89044045116727494800000004479366', 89044045116727494800000004479366]:
+ self.assertTrue(verify_eid_checksum(eid))
+
+ def test_eid_verify_wrong(self):
+ self.assertFalse(verify_eid_checksum('89049032123451234512345678901234'))
+ self.assertFalse(verify_eid_checksum(89049032123451234512345678901234))
+
+ def test_eid_encode_with_32_digits(self):
+ self.assertEqual(compute_eid_checksum('89049032123451234512345678901200'), '89049032123451234512345678901235')
+ self.assertEqual(compute_eid_checksum('89086030202200000022000023022900'), '89086030202200000022000023022943')
+
+ def test_eid_encode_with_30digits(self):
+ self.assertEqual(compute_eid_checksum('890490321234512345123456789012'), '89049032123451234512345678901235')
+
+ def test_eid_encode_with_wrong_csum(self):
+ # input: EID with wrong checksum
+ self.assertEqual(compute_eid_checksum('89049032123451234512345678901299'), '89049032123451234512345678901235')
+ self.assertEqual(compute_eid_checksum(89049032123451234512345678901299), '89049032123451234512345678901235')
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_files.py b/tests/test_files.py
index 3fb1062..9fdb022 100755
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -29,6 +29,7 @@ import pySim.ts_31_103
import pySim.ts_51_011
import pySim.sysmocom_sja2
import pySim.gsm_r
+import pySim.cdma_ruim
def get_qualified_name(c):
"""return the qualified (by module) name of a class."""
@@ -36,6 +37,21 @@ def get_qualified_name(c):
class LinFixed_Test(unittest.TestCase):
classes = all_subclasses(LinFixedEF)
+ maxDiff = None
+
+ @staticmethod
+ def _parse_t(t):
+ """Parse a test description which can either be a 2-tuple of (enc, dec) or
+ a 3-tuple of (enc, rec_nr, dec)."""
+ if len(t) == 2:
+ encoded = t[0]
+ rec_num = 1
+ decoded = t[1]
+ else:
+ encoded = t[0]
+ rec_num = t[1]
+ decoded = t[2]
+ return encoded, rec_num, decoded
def test_decode_record(self):
"""Test the decoder for a linear-fixed EF. Requires the given LinFixedEF subclass
@@ -48,19 +64,20 @@ class LinFixed_Test(unittest.TestCase):
name = get_qualified_name(c)
if hasattr(c, '_test_decode'):
for t in c._test_decode:
+ encoded, rec_num, decoded = self._parse_t(t)
with self.subTest(name, test_decode=t):
inst = c()
- if len(t) == 2:
- encoded = t[0]
- rec_num = 1
- decoded = t[1]
- else:
- encoded = t[0]
- rec_num = t[1]
- decoded = t[2]
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_record_hex(encoded, rec_num)
self.assertEqual(decoded, re_dec)
+ if hasattr(c, '_test_no_pad') and c._test_no_pad:
+ continue
+ with self.subTest(name, test_decode_padded=t):
+ encoded = encoded + 'ff'
+ inst = c()
+ logging.debug("Testing padded decode of %s", name)
+ re_dec = inst.decode_record_hex(encoded, rec_num)
+ self.assertEqual(decoded, re_dec)
def test_encode_record(self):
"""Test the encoder for a linear-fixed EF. Requires the given LinFixedEF subclass
@@ -75,17 +92,10 @@ class LinFixed_Test(unittest.TestCase):
for t in c._test_encode:
with self.subTest(name, test_encode=t):
inst = c()
- if len(t) == 2:
- encoded = t[0]
- rec_num = 1
- decoded = t[1]
- else:
- encoded = t[0]
- rec_num = t[1]
- decoded = t[2]
+ encoded, rec_num, decoded = self._parse_t(t)
logging.debug("Testing encode of %s", name)
re_enc = inst.encode_record_hex(decoded, rec_num)
- self.assertEqual(encoded, re_enc)
+ self.assertEqual(encoded.upper(), re_enc.upper())
def test_de_encode_record(self):
"""Test the decoder and encoder for a linear-fixed EF. Performs first a decoder
@@ -102,27 +112,29 @@ class LinFixed_Test(unittest.TestCase):
name = get_qualified_name(c)
if hasattr(c, '_test_de_encode'):
for t in c._test_de_encode:
+ encoded, rec_num, decoded = self._parse_t(t)
with self.subTest(name, test_de_encode=t):
inst = c()
- if len(t) == 2:
- encoded = t[0]
- rec_num = 1
- decoded = t[1]
- else:
- encoded = t[0]
- rec_num = t[1]
- decoded = t[2]
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_record_hex(encoded, rec_num)
self.assertEqual(decoded, re_dec)
# re-encode the decoded data
logging.debug("Testing re-encode of %s", name)
re_enc = inst.encode_record_hex(re_dec, rec_num)
- self.assertEqual(encoded, re_enc)
+ self.assertEqual(encoded.upper(), re_enc.upper())
+ if hasattr(c, '_test_no_pad') and c._test_no_pad:
+ continue
+ with self.subTest(name, test_decode_padded=t):
+ encoded = encoded + 'ff'
+ inst = c()
+ logging.debug("Testing padded decode of %s", name)
+ re_dec = inst.decode_record_hex(encoded, rec_num)
+ self.assertEqual(decoded, re_dec)
class TransRecEF_Test(unittest.TestCase):
classes = all_subclasses(TransRecEF)
+ maxDiff = None
def test_decode_record(self):
"""Test the decoder for a transparent record-oriented EF. Requires the given TransRecEF subclass
@@ -140,6 +152,8 @@ class TransRecEF_Test(unittest.TestCase):
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_record_hex(encoded)
self.assertEqual(decoded, re_dec)
+ # there's no point in testing padded input, as TransRecEF have a fixed record
+ # size and we cannot ever receive more input data than that size.
def test_encode_record(self):
"""Test the encoder for a transparent record-oriented EF. Requires the given TransRecEF subclass
@@ -181,10 +195,14 @@ class TransRecEF_Test(unittest.TestCase):
# re-encode the decoded data
logging.debug("Testing re-encode of %s", name)
re_enc = inst.encode_record_hex(re_dec)
- self.assertEqual(encoded, re_enc)
+ self.assertEqual(encoded.upper(), re_enc.upper())
+ # there's no point in testing padded input, as TransRecEF have a fixed record
+ # size and we cannot ever receive more input data than that size.
class TransparentEF_Test(unittest.TestCase):
+ maxDiff = None
+
@classmethod
def get_classes(cls):
"""get list of TransparentEF sub-classes which are not a TransRecEF subclass."""
@@ -206,13 +224,21 @@ class TransparentEF_Test(unittest.TestCase):
name = get_qualified_name(c)
if hasattr(c, '_test_decode'):
for t in c._test_decode:
+ encoded = t[0]
+ decoded = t[1]
with self.subTest(name, test_decode=t):
inst = c()
- encoded = t[0]
- decoded = t[1]
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_hex(encoded)
self.assertEqual(decoded, re_dec)
+ if hasattr(c, '_test_no_pad') and c._test_no_pad:
+ continue
+ with self.subTest(name, test_decode_padded=t):
+ encoded = encoded + 'ff'
+ inst = c()
+ logging.debug("Testing padded decode of %s", name)
+ re_dec = inst.decode_hex(encoded)
+ self.assertEqual(decoded, re_dec)
def test_encode_file(self):
"""Test the encoder for a transparent EF. Requires the given TransparentEF subclass
@@ -243,18 +269,25 @@ class TransparentEF_Test(unittest.TestCase):
name = get_qualified_name(c)
if hasattr(c, '_test_de_encode'):
for t in c._test_de_encode:
+ encoded = t[0]
+ decoded = t[1]
with self.subTest(name, test_de_encode=t):
inst = c()
- encoded = t[0]
- decoded = t[1]
logging.debug("Testing decode of %s", name)
re_dec = inst.decode_hex(encoded)
self.assertEqual(decoded, re_dec)
logging.debug("Testing re-encode of %s", name)
re_dec = inst.decode_hex(encoded)
re_enc = inst.encode_hex(re_dec)
- self.assertEqual(encoded, re_enc)
-
+ self.assertEqual(encoded.upper(), re_enc.upper())
+ if hasattr(c, '_test_no_pad') and c._test_no_pad:
+ continue
+ with self.subTest(name, test_decode_padded=t):
+ encoded = encoded + 'ff'
+ inst = c()
+ logging.debug("Testing padded decode of %s", name)
+ re_dec = inst.decode_hex(encoded)
+ self.assertEqual(decoded, re_dec)
if __name__ == '__main__':
logger = logging.getLogger()
diff --git a/tests/test_globalplatform.py b/tests/test_globalplatform.py
new file mode 100644
index 0000000..ba4ad76
--- /dev/null
+++ b/tests/test_globalplatform.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+
+# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import logging
+
+from pySim.global_platform import *
+from pySim.global_platform.scp import *
+from pySim.utils import b2h, h2b
+
+KIC = h2b('100102030405060708090a0b0c0d0e0f') # enc
+KID = h2b('101102030405060708090a0b0c0d0e0f') # MAC
+KIK = h2b('102102030405060708090a0b0c0d0e0f') # DEK
+ck_3des_70 = GpCardKeyset(0x20, KIC, KID, KIK)
+
+class SCP02_Auth_Test(unittest.TestCase):
+ host_challenge = h2b('40A62C37FA6304F8')
+ init_update_resp = h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3')
+
+ def setUp(self):
+ self.scp02 = SCP02(card_keys=ck_3des_70)
+
+ def test_mutual_auth_success(self):
+ init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
+ self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F8')
+ self.scp02.parse_init_update_resp(self.init_update_resp)
+ ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
+ self.assertEqual(b2h(ext_auth_cmd).upper(), '8482010010BA6961667737C5BCEBECE14C7D6A4376')
+
+ def test_mutual_auth_fail_card_cryptogram(self):
+ init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
+ self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F8')
+ wrong_init_update_resp = self.init_update_resp.copy()
+ wrong_init_update_resp[-1:] = b'\xff'
+ with self.assertRaises(ValueError):
+ self.scp02.parse_init_update_resp(wrong_init_update_resp)
+
+
+class SCP02_Test(unittest.TestCase):
+ host_challenge = h2b('40A62C37FA6304F8')
+ init_update_resp = h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3')
+
+ def setUp(self):
+ self.scp02 = SCP02(card_keys=ck_3des_70)
+ init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
+ self.scp02.parse_init_update_resp(self.init_update_resp)
+ ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
+
+ def test_mac_command(self):
+ wrapped = self.scp02.wrap_cmd_apdu(h2b('80f28002024f00'))
+ self.assertEqual(b2h(wrapped).upper(), '84F280020A4F00B21AAFA3EB2D1672')
+
+
+class SCP03_Test:
+ """some kind of 'abstract base class' for a unittest.UnitTest, implementing common functionality for all
+ of our SCP03 test caseses."""
+ get_eid_cmd_plain = h2b('80E2910006BF3E035C015A')
+ get_eid_rsp_plain = h2b('bf3e125a1089882119900000000000000000000005')
+
+ # must be overridden by derived classes
+ init_upd_cmd = b''
+ init_upd_rsp = b''
+ ext_auth_cmd = b''
+ get_eid_cmd = b''
+ get_eid_rsp = b''
+ keyset = None
+
+ @property
+ def host_challenge(self) -> bytes:
+ return self.init_upd_cmd[5:]
+
+ @property
+ def kvn(self) -> int:
+ return self.init_upd_cmd[2]
+
+ @property
+ def security_level(self) -> int:
+ return self.ext_auth_cmd[2]
+
+ @property
+ def card_challenge(self) -> bytes:
+ if len(self.init_upd_rsp) in [10+3+8+8, 10+3+8+8+3]:
+ return self.init_upd_rsp[10+3:10+3+8]
+ else:
+ return self.init_upd_rsp[10+3:10+3+16]
+
+ @property
+ def card_cryptogram(self) -> bytes:
+ if len(self.init_upd_rsp) in [10+3+8+8, 10+3+8+8+3]:
+ return self.init_upd_rsp[10+3+8:10+3+8+8]
+ else:
+ return self.init_upd_rsp[10+3+16:10+3+16+16]
+
+ @classmethod
+ def setUpClass(cls):
+ cls.scp = SCP03(card_keys = cls.keyset)
+
+ def test_01_initialize_update(self):
+ # pylint: disable=no-member
+ self.assertEqual(self.init_upd_cmd, self.scp.gen_init_update_apdu(self.host_challenge))
+
+ def test_02_parse_init_upd_resp(self):
+ self.scp.parse_init_update_resp(self.init_upd_rsp)
+
+ def test_03_gen_ext_auth_apdu(self):
+ # pylint: disable=no-member
+ self.assertEqual(self.ext_auth_cmd, self.scp.gen_ext_auth_apdu(self.security_level))
+
+ def test_04_wrap_cmd_apdu_get_eid(self):
+ # pylint: disable=no-member
+ self.assertEqual(self.get_eid_cmd, self.scp.wrap_cmd_apdu(self.get_eid_cmd_plain))
+
+ def test_05_unwrap_rsp_apdu_get_eid(self):
+ # pylint: disable=no-member
+ self.assertEqual(self.get_eid_rsp_plain, self.scp.unwrap_rsp_apdu(h2b('9000'), self.get_eid_rsp))
+
+
+# The SCP03 keysets used for various key lenghs
+KEYSET_AES128 = GpCardKeyset(0x30, h2b('000102030405060708090a0b0c0d0e0f'), h2b('101112131415161718191a1b1c1d1e1f'), h2b('202122232425262728292a2b2c2d2e2f'))
+KEYSET_AES192 = GpCardKeyset(0x31, h2b('000102030405060708090a0b0c0d0e0f0001020304050607'),
+ h2b('101112131415161718191a1b1c1d1e1f1011121314151617'), h2b('202122232425262728292a2b2c2d2e2f2021222324252627'))
+KEYSET_AES256 = GpCardKeyset(0x32, h2b('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f'),
+ h2b('101112131415161718191a1b1c1d1e1f101112131415161718191a1b1c1d1e1f'),
+ h2b('202122232425262728292a2b2c2d2e2f202122232425262728292a2b2c2d2e2f'))
+
+class SCP03_Test_AES128_11(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES128
+ init_upd_cmd = h2b('8050300008b13e5f938fc108c4')
+ init_upd_rsp = h2b('000000000000000000003003703eb51047495b249f66c484c1d2ef1948000002')
+ ext_auth_cmd = h2b('84821100107d5f5826a993ebc89eea24957fa0b3ce')
+ get_eid_cmd = h2b('84e291000ebf3e035c015a558d036518a28297')
+ get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005971be68992dbbdfa')
+
+class SCP03_Test_AES128_03(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES128
+ init_upd_cmd = h2b('80503000088e1552d0513c60f3')
+ init_upd_rsp = h2b('0000000000000000000030037030760cd2c47c1dd395065fe5ead8a9d7000001')
+ ext_auth_cmd = h2b('8482030010fd4721a14d9b07003c451d2f8ae6bb21')
+ get_eid_cmd = h2b('84e2910018ca9c00f6713d79bc8baa642bdff51c3f6a4082d3bd9ad26c')
+ get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
+
+class SCP03_Test_AES128_33(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES128
+ init_upd_cmd = h2b('8050300008fdf38259a1e0de44')
+ init_upd_rsp = h2b('000000000000000000003003703b1aca81e821f219081cdc01c26b372d000003')
+ ext_auth_cmd = h2b('84823300108c36f96bcc00724a4e13ad591d7da3f0')
+ get_eid_cmd = h2b('84e2910018267a85dfe4a98fca6fb0527e0dfecce4914e40401433c87f')
+ get_eid_rsp = h2b('f3ba2b1013aa6224f5e1c138d71805c569e5439b47576260b75fc021b25097cb2e68f8a0144975b9')
+
+class SCP03_Test_AES192_11(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES192
+ init_upd_cmd = h2b('80503100087396430b768b085b')
+ init_upd_rsp = h2b('000000000000000000003103708cfc23522ffdbf1e5df5542cac8fd866000003')
+ ext_auth_cmd = h2b('84821100102145ed30b146f5db252fb7e624cec244')
+ get_eid_cmd = h2b('84e291000ebf3e035c015aff42cf801d143944')
+ get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005162fbd33e04940a9')
+
+class SCP03_Test_AES192_03(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES192
+ init_upd_cmd = h2b('805031000869c65da8202bf19f')
+ init_upd_rsp = h2b('00000000000000000000310370b570a67be38446717729d6dd3d2ec5b1000001')
+ ext_auth_cmd = h2b('848203001065df4f1a356a887905466516d9e5b7c1')
+ get_eid_cmd = h2b('84e2910018d2c6fb477c5d4afe4fd4d21f17eff10d3578ec1774a12a2d')
+ get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
+
+class SCP03_Test_AES192_33(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES192
+ init_upd_cmd = h2b('80503100089b3f2eef0e8c9374')
+ init_upd_rsp = h2b('00000000000000000000310370f6bb305a15bae1a68f79fb08212fbed7000002')
+ ext_auth_cmd = h2b('84823300109100bc22d58b45b86a26365ce39ff3cf')
+ get_eid_cmd = h2b('84e29100188f7f946c84f70d17994bc6e8791251bb1bb1bf02cf8de589')
+ get_eid_rsp = h2b('c05176c1b6f72aae50c32cbee63b0e95998928fd4dfb2be9f27ffde8c8476f5909b4805cc4039599')
+
+class SCP03_Test_AES256_11(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES256
+ init_upd_cmd = h2b('805032000811666d57866c6f54')
+ init_upd_rsp = h2b('0000000000000000000032037053ea8847efa7674e41498a4d66cf0dee000003')
+ ext_auth_cmd = h2b('84821100102f2ad190eff2fafc4908996d1cebd310')
+ get_eid_cmd = h2b('84e291000ebf3e035c015af4b680372542b59d')
+ get_eid_rsp = h2b('bf3e125a10898821199000000000000000000000058012dd7f01f1c4c1')
+
+class SCP03_Test_AES256_03(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES256
+ init_upd_cmd = h2b('8050320008c6066990fc426e1d')
+ init_upd_rsp = h2b('000000000000000000003203708682cd81bbd8919f2de3f2664581f118000001')
+ ext_auth_cmd = h2b('848203001077c493b632edadaf865a1e64acc07ce9')
+ get_eid_cmd = h2b('84e29100183ddaa60594963befaada3525b492ede23c2ab2c1ce3afe44')
+ get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
+
+class SCP03_Test_AES256_33(SCP03_Test, unittest.TestCase):
+ keyset = KEYSET_AES256
+ init_upd_cmd = h2b('805032000897b2055fe58599fd')
+ init_upd_rsp = h2b('00000000000000000000320370a8439a22cedf045fa9f1903b2834f26e000002')
+ ext_auth_cmd = h2b('8482330010508a0fd959d2e547c6b33154a6be2057')
+ get_eid_cmd = h2b('84e29100187a5ef717eaf1e135ae92fe54429d0e465decda65f5fe5aea')
+ get_eid_rsp = h2b('ea90dbfa648a67c5eb6abc57f8530b97d0cd5647c5e8732016b55203b078dd2ace7f8bc5d1c1cd99')
+
+# FIXME:
+# - for S8 and S16 mode
+# FIXME: test auth with random (0x60) vs pseudo-random (0x70) challenge
+
+
+class SCP03_KCV_Test(unittest.TestCase):
+ def test_kcv(self):
+ self.assertEqual(compute_kcv('aes', KEYSET_AES128.enc), h2b('C35280'))
+ self.assertEqual(compute_kcv('aes', KEYSET_AES128.mac), h2b('013808'))
+ self.assertEqual(compute_kcv('aes', KEYSET_AES128.dek), h2b('840DE5'))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_ota.py b/tests/test_ota.py
index 7de5cff..ce10a7d 100644
--- a/tests/test_ota.py
+++ b/tests/test_ota.py
@@ -270,8 +270,8 @@ class SmsOtaTestCase(OtaTestCase):
tpdu = SMS_DELIVER(tp_udhi=True, tp_oa=self.da, tp_pid=0x7F, tp_dcs=0xF6,
tp_scts=h2b('22705200000000'), tp_udl=3, tp_ud=with_udh)
#print("TPDU: %s" % tpdu)
- #print("tpdu: %s" % b2h(tpdu.toBytes()))
- self.assertEqual(b2h(tpdu.toBytes()), t['request']['encoded_tpdu'])
+ #print("tpdu: %s" % b2h(tpdu.to_bytes()))
+ self.assertEqual(b2h(tpdu.to_bytes()), t['request']['encoded_tpdu'])
def test_decode_resp(self):
for t in SmsOtaTestCase.testdatasets:
diff --git a/tests/test_sms.py b/tests/test_sms.py
index 8355224..7130399 100644
--- a/tests/test_sms.py
+++ b/tests/test_sms.py
@@ -6,7 +6,7 @@ from pySim.sms import *
class Test_SMS_UDH(unittest.TestCase):
def test_single_ie(self):
- udh, tail = UserDataHeader.fromBytes('027100')
+ udh, tail = UserDataHeader.from_bytes('027100')
self.assertEqual(len(udh.ies), 1)
ie = udh.ies[0]
self.assertEqual(ie.iei, 0x71)
@@ -15,7 +15,7 @@ class Test_SMS_UDH(unittest.TestCase):
self.assertEqual(tail, b'')
def test_single_ie_tail(self):
- udh, tail = UserDataHeader.fromBytes('027100abcdef')
+ udh, tail = UserDataHeader.from_bytes('027100abcdef')
self.assertEqual(len(udh.ies), 1)
ie = udh.ies[0]
self.assertEqual(ie.iei, 0x71)
@@ -24,7 +24,7 @@ class Test_SMS_UDH(unittest.TestCase):
self.assertEqual(tail, b'\xab\xcd\xef')
def test_single_ie_value(self):
- udh, tail = UserDataHeader.fromBytes('03710110')
+ udh, tail = UserDataHeader.from_bytes('03710110')
self.assertEqual(len(udh.ies), 1)
ie = udh.ies[0]
self.assertEqual(ie.iei, 0x71)
@@ -33,7 +33,7 @@ class Test_SMS_UDH(unittest.TestCase):
self.assertEqual(tail, b'')
def test_two_ie_data_tail(self):
- udh, tail = UserDataHeader.fromBytes('0571007001ffabcd')
+ udh, tail = UserDataHeader.from_bytes('0571007001ffabcd')
self.assertEqual(len(udh.ies), 2)
ie = udh.ies[0]
self.assertEqual(ie.iei, 0x71)
@@ -45,42 +45,42 @@ class Test_SMS_UDH(unittest.TestCase):
self.assertEqual(ie.value, b'\xff')
self.assertEqual(tail, b'\xab\xcd')
- def test_toBytes(self):
+ def test_to_bytes(self):
indata = h2b('0571007001ff')
- udh, tail = UserDataHeader.fromBytes(indata)
- encoded = udh.toBytes()
+ udh, tail = UserDataHeader.from_bytes(indata)
+ encoded = udh.to_bytes()
self.assertEqual(encoded, indata)
class Test_AddressField(unittest.TestCase):
- def test_fromBytes(self):
+ def test_from_bytes(self):
encoded = h2b('0480214399')
- af, trailer = AddressField.fromBytes(encoded)
+ af, trailer = AddressField.from_bytes(encoded)
self.assertEqual(trailer, b'\x99')
self.assertEqual(af.ton, 'unknown')
self.assertEqual(af.npi, 'unknown')
self.assertEqual(af.digits, '1234')
- def test_fromBytes_odd(self):
- af, trailer = AddressField.fromBytes('038021f399')
+ def test_from_bytes_odd(self):
+ af, trailer = AddressField.from_bytes('038021f399')
self.assertEqual(trailer, b'\x99')
self.assertEqual(af.ton, 'unknown')
self.assertEqual(af.npi, 'unknown')
self.assertEqual(af.digits, '123')
- def test_toBytes(self):
+ def test_to_bytes(self):
encoded = h2b('04802143')
- af, trailer = AddressField.fromBytes(encoded)
- self.assertEqual(af.toBytes(), encoded)
+ af, trailer = AddressField.from_bytes(encoded)
+ self.assertEqual(af.to_bytes(), encoded)
- def test_toBytes_odd(self):
+ def test_to_bytes_odd(self):
af = AddressField('12345', 'international', 'isdn_e164')
- encoded = af.toBytes()
+ encoded = af.to_bytes()
self.assertEqual(encoded, h2b('05912143f5'))
class Test_SUBMIT(unittest.TestCase):
- def test_fromBytes(self):
- s = SMS_SUBMIT.fromBytes('550d0b911614261771f000f5a78c0b050423f423f40003010201424547494e3a56434152440d0a56455253494f4e3a322e310d0a4e3a4d650d0a54454c3b505245463b43454c4c3b564f4943453a2b36313431363237313137300d0a54454c3b484f4d453b564f4943453a2b36313339353337303437310d0a54454c3b574f524b3b564f4943453a2b36313339363734373031350d0a454e443a')
+ def test_from_bytes(self):
+ s = SMS_SUBMIT.from_bytes('550d0b911614261771f000f5a78c0b050423f423f40003010201424547494e3a56434152440d0a56455253494f4e3a322e310d0a4e3a4d650d0a54454c3b505245463b43454c4c3b564f4943453a2b36313431363237313137300d0a54454c3b484f4d453b564f4943453a2b36313339353337303437310d0a54454c3b574f524b3b564f4943453a2b36313339363734373031350d0a454e443a')
self.assertEqual(s.tp_mti, 1)
self.assertEqual(s.tp_rd, True)
self.assertEqual(s.tp_vpf, 'relative')
@@ -92,8 +92,8 @@ class Test_SUBMIT(unittest.TestCase):
self.assertEqual(s.tp_udl, 140)
class Test_DELIVER(unittest.TestCase):
- def test_fromBytes(self):
- d = SMS_DELIVER.fromBytes('0408D0E5759A0E7FF6907090307513000824010101BB400101')
+ def test_from_bytes(self):
+ d = SMS_DELIVER.from_bytes('0408D0E5759A0E7FF6907090307513000824010101BB400101')
self.assertEqual(d.tp_mti, 0)
self.assertEqual(d.tp_mms, True)
self.assertEqual(d.tp_lp, False)
diff --git a/tests/test_tlv.py b/tests/test_tlv.py
index 1e5000d..0e73ab1 100644
--- a/tests/test_tlv.py
+++ b/tests/test_tlv.py
@@ -17,6 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
+from construct import Int8ub
from pySim.tlv import *
class TestUtils(unittest.TestCase):
diff --git a/tests/test_tlvs.py b/tests/test_tlvs.py
new file mode 100755
index 0000000..a0dd107
--- /dev/null
+++ b/tests/test_tlvs.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+
+# (C) 2023 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import logging
+
+from pySim.utils import b2h, h2b, all_subclasses
+from pySim.tlv import *
+
+import pySim.iso7816_4
+import pySim.ts_102_221
+import pySim.ts_102_222
+import pySim.ts_31_102
+import pySim.ts_31_103
+import pySim.ts_51_011
+import pySim.sysmocom_sja2
+import pySim.gsm_r
+import pySim.cdma_ruim
+import pySim.global_platform
+
+if 'unittest.util' in __import__('sys').modules:
+ # Show full diff in self.assertEqual.
+ __import__('sys').modules['unittest.util']._MAX_LENGTH = 999999999
+
+def get_qualified_name(c):
+ """return the qualified (by module) name of a class."""
+ return "%s.%s" % (c.__module__, c.__name__)
+
+class TLV_IE_Test(unittest.TestCase):
+ maxDiff = None
+
+ @classmethod
+ def get_classes(cls):
+ """get list of TLV_IE sub-classes."""
+ return all_subclasses(TLV_IE)
+
+ @classmethod
+ def setUpClass(cls):
+ """set-up method called once for this class by unittest framework"""
+ cls.classes = cls.get_classes()
+
+ def test_decode_tlv(self):
+ """Test the decoder for a TLV_IE. Requires the given TLV_IE subclass
+ to have a '_test_decode' attribute, containing a list of tuples. Each tuple
+ is a 2-tuple (hexstring, decoded_dict).
+ """
+ for c in self.classes:
+ name = get_qualified_name(c)
+ if hasattr(c, '_test_decode'):
+ for t in c._test_decode:
+ with self.subTest(name, test_decode=t):
+ inst = c()
+ encoded = t[0]
+ decoded = { camel_to_snake(c.__name__): t[1] }
+ context = t[2] if len(t) == 3 else {}
+ logging.debug("Testing decode of %s", name)
+ inst.from_tlv(h2b(encoded), context=context)
+ re_dec = inst.to_dict()
+ self.assertEqual(decoded, re_dec)
+
+ def test_encode_tlv(self):
+ """Test the encoder for a TLV_IE. Requires the given TLV_IE subclass
+ to have a '_test_encode' attribute, containing a list of tuples. Each tuple
+ is a 2-tuple (hexstring, decoded_dict).
+ """
+ for c in self.classes:
+ name = get_qualified_name(c)
+ if hasattr(c, '_test_encode'):
+ for t in c._test_encode:
+ with self.subTest(name, test_encode=t):
+ inst = c()
+ encoded = t[0]
+ decoded = { camel_to_snake(c.__name__): t[1] }
+ context = t[2] if len(t) == 3 else {}
+ logging.debug("Testing encode of %s", name)
+ inst.from_dict(decoded)
+ re_enc = b2h(inst.to_tlv(context))
+ self.assertEqual(encoded.upper(), re_enc.upper())
+
+ def test_de_encode_tlv(self):
+ """Test the decoder and encoder for a TLV_IE. Performs first a decoder
+ test, and then re-encodes the decoded data, comparing the re-encoded data with the
+ initial input data.
+
+ Requires the given TLV_IE subclass to have a '_test_de_encode' attribute,
+ containing a list of tuples. Each tuple is a 2-tuple (hexstring, decoded_dict).
+ """
+ for c in self.classes:
+ name = get_qualified_name(c)
+ if hasattr(c, '_test_de_encode'):
+ for t in c._test_de_encode:
+ with self.subTest(name, test_de_encode=t):
+ inst = c()
+ encoded = t[0]
+ decoded = { camel_to_snake(c.__name__): t[1] }
+ context = t[2] if len(t) == 3 else {}
+ logging.debug("Testing decode of %s", name)
+ inst.from_tlv(h2b(encoded), context=context)
+ re_dec = inst.to_dict()
+ self.assertEqual(decoded, re_dec)
+ logging.debug("Testing re-encode of %s", name)
+ re_enc = b2h(inst.to_tlv(context=context))
+ self.assertEqual(encoded.upper(), re_enc.upper())
+
+
+if __name__ == '__main__':
+ logger = logging.getLogger()
+ logger.setLevel(logging.DEBUG)
+ unittest.main()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b7f790d..2c24e3e 100755
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,6 +2,7 @@
import unittest
from pySim import utils
+from pySim.legacy import utils as legacy_utils
from pySim.ts_31_102 import EF_SUCI_Calc_Info
# we don't really want to thest TS 102 221, but the underlying DataObject codebase
@@ -45,7 +46,7 @@ class DecTestCase(unittest.TestCase):
"ffffff0002",
"ffffff0001",
]
- self.assertEqual(utils.hexstr_to_Nbytearr(input_str, 5), expected)
+ self.assertEqual(legacy_utils.hexstr_to_Nbytearr(input_str, 5), expected)
def testDecMCCfromPLMN(self):
self.assertEqual(utils.dec_mcc_from_plmn("92f501"), 295)
@@ -94,13 +95,13 @@ class DecTestCase(unittest.TestCase):
self.assertEqual(utils.dec_act("8000"), ["UTRAN"])
def testDecAct_onlyEUtran(self):
- self.assertEqual(utils.dec_act("4000"), ["E-UTRAN"])
+ self.assertEqual(utils.dec_act("4000"), ["E-UTRAN NB-S1", "E-UTRAN WB-S1"])
def testDecAct_onlyNgRan(self):
self.assertEqual(utils.dec_act("0800"), ["NG-RAN"])
def testDecAct_onlyGsm(self):
- self.assertEqual(utils.dec_act("0080"), ["GSM"])
+ self.assertEqual(utils.dec_act("0084"), ["GSM"])
def testDecAct_onlyGsmCompact(self):
self.assertEqual(utils.dec_act("0040"), ["GSM COMPACT"])
@@ -112,7 +113,7 @@ class DecTestCase(unittest.TestCase):
self.assertEqual(utils.dec_act("0010"), ["cdma2000 1xRTT"])
def testDecAct_allSet(self):
- self.assertEqual(utils.dec_act("ffff"), ["UTRAN", "E-UTRAN WB-S1", "E-UTRAN NB-S1", "NG-RAN", "GSM", "GSM COMPACT", "cdma2000 HRPD", "cdma2000 1xRTT"])
+ self.assertEqual(utils.dec_act("ffff"), ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'GSM COMPACT', 'NG-RAN', 'UTRAN', 'cdma2000 1xRTT', 'cdma2000 HRPD'])
def testDecxPlmn_w_act(self):
expected = {'mcc': '295', 'mnc': '10', 'act': ["UTRAN"]}
@@ -130,17 +131,17 @@ class DecTestCase(unittest.TestCase):
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
expected += "\tffffff0000 # unused\n"
- self.assertEqual(utils.format_xplmn_w_act(input_str), expected)
+ self.assertEqual(legacy_utils.format_xplmn_w_act(input_str), expected)
def testDecodeSuciCalcInfo(self):
suci_calc_info = EF_SUCI_Calc_Info()
- decoded = suci_calc_info._decode_hex(self.testfile_suci_calc_info)
+ decoded = suci_calc_info.decode_hex(self.testfile_suci_calc_info)
self.assertDictEqual(self.decoded_testfile_suci, decoded)
def testEncodeSuciCalcInfo(self):
suci_calc_info = EF_SUCI_Calc_Info()
- encoded = suci_calc_info._encode_hex(self.decoded_testfile_suci)
+ encoded = suci_calc_info.encode_hex(self.decoded_testfile_suci)
self.assertEqual(encoded.lower(), self.testfile_suci_calc_info.lower())
def testEnc_msisdn(self):
@@ -221,5 +222,36 @@ class TestComprTlv(unittest.TestCase):
res = utils.comprehensiontlv_encode_tag({'tag': 0x1234, 'comprehension':True})
self.assertEqual(res, b'\x7f\x92\x34')
+class TestDgiTlv(unittest.TestCase):
+ def test_DgiTlvLenEnc(self):
+ self.assertEqual(utils.dgi_encode_len(10), b'\x0a')
+ self.assertEqual(utils.dgi_encode_len(254), b'\xfe')
+ self.assertEqual(utils.dgi_encode_len(255), b'\xff\x00\xff')
+ self.assertEqual(utils.dgi_encode_len(65535), b'\xff\xff\xff')
+ with self.assertRaises(ValueError):
+ utils.dgi_encode_len(65536)
+
+ def testDgiTlvLenDec(self):
+ self.assertEqual(utils.dgi_parse_len(b'\x0a\x0b'), (10, b'\x0b'))
+ self.assertEqual(utils.dgi_parse_len(b'\xfe\x0b'), (254, b'\x0b'))
+ self.assertEqual(utils.dgi_parse_len(b'\xff\x00\xff\x0b'), (255, b'\x0b'))
+
+class TestLuhn(unittest.TestCase):
+ def test_verify(self):
+ utils.verify_luhn('8988211000000530082')
+
+ def test_encode(self):
+ self.assertEqual(utils.calculate_luhn('898821100000053008'), 2)
+
+ def test_sanitize_iccid(self):
+ # 19 digits with correct luhn; we expect no change
+ self.assertEqual(utils.sanitize_iccid('8988211000000530082'), '8988211000000530082')
+ # 20 digits with correct luhn; we expect no change
+ self.assertEqual(utils.sanitize_iccid('89882110000005300811'), '89882110000005300811')
+ # 19 digits without correct luhn; we expect check digit to be added
+ self.assertEqual(utils.sanitize_iccid('8988211000000530081'), '89882110000005300811')
+ # 18 digits; we expect luhn check digit to be added
+ self.assertEqual(utils.sanitize_iccid('898821100000053008'), '8988211000000530082')
+
if __name__ == "__main__":
unittest.main()