Compare commits

..

14 Commits

Author SHA1 Message Date
Christian Amsüss
3cec0150ee Merge branch 'chrysn/for-29033' into chrysn/ota
Change-Id: Ie0d9b0c04fe618d3809e117823c519b28d15ea05
2022-08-19 23:02:20 +02:00
Christian Amsüss
80b535e424 OTA: Return both packet and decoded at decryption; do not attempt to decode absent data
Change-Id: I1c488a19ad75114255c8ff1c59d903388451b37a
2022-08-19 23:01:52 +02:00
Christian Amsüss
28af6966aa Merge branch 'chrysn/for-29033' into chrysn/ota
Change-Id: Iebc7444fc32184d24c7bccc8ce694755cbbcc04b
2022-08-19 20:37:56 +02:00
Christian Amsüss
30439fd9b8 proactive: Send a Terminal Response automatically after a Fetch
Change-Id: I43bc994e7517b5907fb40a98d84797c54056c47d
2022-08-19 20:37:38 +02:00
Christian Amsüss
707ecf0f27 proactive: Avoid clobbering the output of the command that triggered the FETCH
Change-Id: I2b794a5c5bc808b9703b4bc679c119341a0ed41c
2022-08-19 20:37:38 +02:00
Christian Amsüss
fd290f9793 SMS: UserDataHeader's __str__ is also a suitable __repr__
Change-Id: I59cc04e8700a374b440f192080b5c5baad60a235
2022-08-19 20:29:48 +02:00
Christian Amsüss
958f0384db OTA: Set PCNT (padding count) in messages
Change-Id: Ia3cb4222ebe7f8922c96509a94cf126db3dea14c
2022-08-19 20:23:43 +02:00
Harald Welte
b5d143d961 [UNTESTED] sysmocom_sja2: Support files related to OTA HTTPS features
Change-Id: I5710b82c2eea6b6bf5b38882b2a1ec7d60a725d8
2022-08-12 16:58:43 +02:00
Harald Welte
8df793a8bd WIP: vpcd2smpp.py
Change-Id: I501f2fea075706df379a4bd65a7c6bc19f48277f
2022-08-12 16:58:43 +02:00
Harald Welte
2241e72ecd WIP: smpp2sim
Change-Id: Ie5bae9d823bca6f6c658bd455303f63bace2258c
2022-08-12 16:58:43 +02:00
Harald Welte
755bb2dcfc WIP: classic SIM (3GPP TS 51.011) support.
Change-Id: I1cbbbabd22a67048f3ee9330c12f72c34152ce45
2022-08-12 16:58:43 +02:00
Harald Welte
763f2dfde8 tag version 1.1 of pySim
Closes: OS#5458
Change-Id: Id78fb776eeab3370663cccefaf3fb31e5b2f5b04
2022-08-12 16:58:43 +02:00
Harald Welte
66717dfc45 WIP SMS
Change-Id: I0d95e62c1e7183a7851d1fe38df0f5133830cb1f
2022-08-12 16:58:43 +02:00
Harald Welte
63054b0d36 Add new pySim.ota library, implement SIM OTA crypto
This introduces a hierarchy of classes implementing

* ETS TS 102 225 (general command structure)
* 3GPP TS 31.115 (dialects for SMS-PP)

In this initial patch only the SMS "dialect" is supported,
but it is foreseen that USSD/SMSCB/HTTPS dialects can be
added at a later point.

Change-Id: I193ff4712c8503279c017b4b1324f0c3d38b9f84
2022-08-12 12:46:05 +02:00
393 changed files with 8542 additions and 67894 deletions

View File

@@ -1,2 +0,0 @@
--exclude ^pySim/esim/asn1/.*\.asn$
--exclude ^smdpp-data/.*$

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
open_collective: osmocom

7
.gitignore vendored
View File

@@ -1,9 +1,2 @@
*.pyc
.*.swp
/docs/_*
/docs/generated
/.cache
/.local
/build
/pySim.egg-info

212
README.md
View File

@@ -1,32 +1,96 @@
pySim - Read, Write and Browse Programmable SIM/USIM/ISIM/HPSIM Cards
=====================================================================
pySim - Read, Write and Browse Programmable SIM/USIM Cards
====================================================
This repository contains a number of Python programs that can be used
to read, program (write) and browse all fields/parameters/files on
SIM/USIM/ISIM/HPSIM cards used in 3GPP cellular networks from 2G to 5G.
This repository contains Python programs that can be used
to read, program (write) and browse certain fields/parameters on so-called programmable
SIM/USIM cards.
Note that the access control configuration of normal production cards
issue by operators will restrict significantly which files a normal
user can read, and particularly write to.
The full functionality of pySim hence can only be used with on so-called
programmable SIM/USIM/ISIM/HPSIM cards.
Such SIM/USIM/ISIM/HPSIM cards are special cards, which - unlike those
issued by regular commercial operators - come with the kind of keys that
allow you to write the files/fields that normally only an operator can
program.
Such SIM/USIM cards are special cards, which - unlike those issued by
regular commercial operators - come with the kind of keys that allow you
to write the files/fields that normally only an operator can program.
This is useful particularly if you are running your own cellular
network, and want to configure your own SIM/USIM/ISIM/HPSIM cards for
that network.
network, and want to issue your own SIM/USIM cards for that network.
Homepage
--------
Homepage and Manual
-------------------
Please visit the [official homepage](https://osmocom.org/projects/pysim/wiki)
for usage instructions, manual and examples.
Please visit the [official homepage](https://osmocom.org/projects/pysim/wiki) for usage instructions, manual and examples. The user manual can also be built locally from this source code by ``cd docs && make html latexpdf`` for HTML and PDF format, respectively.
Git Repository
--------------
You can clone from the official Osmocom git repository using
```
git clone git://git.osmocom.org/pysim.git
```
There is a cgit interface at <https://git.osmocom.org/pysim>
Installation
------------
Please install the following dependencies:
- pyscard
- serial
- pytlv
- cmd2 >= 1.3.0 but < 2.0.0
- jsonpath-ng
- construct
- bidict
- gsm0338
Example for Debian:
```
apt-get install python3-pyscard python3-serial python3-pip python3-yaml python3-termcolor python3-colorlog
pip3 install -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.
### Archlinux Package
Archlinux users may install the package ``python-pysim-git``
[![](https://img.shields.io/aur/version/python-pysim-git)](https://aur.archlinux.org/packages/python-pysim-git)
from the [Arch User Repository (AUR)](https://aur.archlinux.org).
The most convenient way is the use of an [AUR Helper](https://wiki.archlinux.org/index.php/AUR_helpers),
e.g. [yay](https://aur.archlinux.org/packages/yay) or [pacaur](https://aur.archlinux.org/packages/pacaur).
The following example shows the installation with ``yay``.
```sh
# Install
yay -Sy python-pysim-git
# Uninstall
sudo pacman -Rs python-pysim-git
```
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
options and the list archive.
Please observe the [Osmocom Mailing List
Rules](https://osmocom.org/projects/cellular-infrastructure/wiki/Mailing_List_Rules)
when posting.
Contributing
------------
Our coding standards are described at
<https://osmocom.org/projects/cellular-infrastructure/wiki/Coding_standards>
We are using a gerrit-based patch review process explained at
<https://osmocom.org/projects/cellular-infrastructure/wiki/Gerrit>
Documentation
@@ -56,107 +120,3 @@ they have very specific requirements like batch programming of large
quantities of cards, which is about the only remaining use case for the
legacy tools.
Git Repository
--------------
You can clone from the official Osmocom git repository using
```
git clone https://gitea.osmocom.org/sim-card/pysim.git
```
There is a web interface at <https://gitea.osmocom.org/sim-card/pysim>.
Installation
------------
Please install the following dependencies:
- bidict
- cmd2 >= 1.5.0
- colorlog
- construct >= 2.9.51
- pyosmocom
- jsonpath-ng
- packaging
- pycryptodomex
- pyscard
- pyserial
- pytlv
- pyyaml >= 5.1
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
- termcolor
Example for Debian:
```sh
sudo apt-get install --no-install-recommends \
pcscd libpcsclite-dev \
python3 \
python3-setuptools \
python3-pycryptodome \
python3-pyscard \
python3-pip
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``
[![](https://img.shields.io/aur/version/python-pysim-git)](https://aur.archlinux.org/packages/python-pysim-git)
from the [Arch User Repository (AUR)](https://aur.archlinux.org).
The most convenient way is the use of an [AUR Helper](https://wiki.archlinux.org/index.php/AUR_helpers),
e.g. [yay](https://aur.archlinux.org/packages/yay) or [pacaur](https://aur.archlinux.org/packages/pacaur).
The following example shows the installation with ``yay``.
```sh
# Install
yay -Sy python-pysim-git
# Uninstall
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 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
------------
Our coding standards are described at
<https://osmocom.org/projects/cellular-infrastructure/wiki/Coding_standards>
We are using a gerrit-based patch review process explained at
<https://osmocom.org/projects/cellular-infrastructure/wiki/Gerrit>

View File

@@ -1,79 +0,0 @@
#!/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 osmocom.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()

View File

@@ -1,73 +0,0 @@
#!/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)

View File

@@ -1,79 +0,0 @@
#!/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)

View File

@@ -1,318 +0,0 @@
#!/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 os
import sys
import argparse
import logging
import hashlib
from typing import List
from urllib.parse import urlparse
from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives.asymmetric import ec
from osmocom.utils import h2b, b2h, swap_nibbles, is_hexstr
from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv
import pySim.esim.rsp as rsp
from pySim.esim import es9p, PMO
from pySim.esim.x509_cert import CertAndPrivkey
from pySim.esim.es8p import BoundProfilePackage
logging.basicConfig(level=logging.DEBUG)
parser = argparse.ArgumentParser(description="""
Utility to manually issue requests against the ES9+ API of an SM-DP+ according to GSMA SGP.22.""")
parser.add_argument('--url', required=True, help='Base URL of ES9+ API endpoint')
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.""")
parser.add_argument('--certificate-path', default='.',
help="Path in which to look for certificate and key files.")
parser.add_argument('--euicc-certificate', default='CERT_EUICC_ECDSA_NIST.der',
help="File name of DER-encoded eUICC certificate file.")
parser.add_argument('--euicc-private-key', default='SK_EUICC_ECDSA_NIST.pem',
help="File name of PEM-format eUICC secret key file.")
parser.add_argument('--eum-certificate', default='CERT_EUM_ECDSA_NIST.der',
help="File name of DER-encoded EUM certificate file.")
parser.add_argument('--ci-certificate', default='CERT_CI_ECDSA_NIST.der',
help="File name of DER-encoded CI certificate file.")
subparsers = parser.add_subparsers(dest='command',help="The command (API function) to call", required=True)
# download
parser_dl = subparsers.add_parser('download', help="ES9+ download")
parser_dl.add_argument('--matchingId', required=True,
help='MatchingID that shall be used by profile download')
parser_dl.add_argument('--output-path', default='.',
help="Path to which the output files will be written.")
parser_dl.add_argument('--confirmation-code',
help="Confirmation Code for the eSIM download")
# notification
parser_ntf = subparsers.add_parser('notification', help='ES9+ (other) notification')
parser_ntf.add_argument('operation', choices=['enable','disable','delete'],
help='Profile Management Opreation whoise occurrence shall be notififed')
parser_ntf.add_argument('--sequence-nr', type=int, required=True,
help='eUICC global notification sequence number')
parser_ntf.add_argument('--notification-address', help='notificationAddress, if different from URL')
parser_ntf.add_argument('--iccid', type=is_hexstr, help='ICCID to which the notification relates')
# notification-install
parser_ntfi = subparsers.add_parser('notification-install', help='ES9+ installation notification')
parser_ntfi.add_argument('--sequence-nr', type=int, required=True,
help='eUICC global notification sequence number')
parser_ntfi.add_argument('--transaction-id', required=True,
help='transactionId of previous ES9+ download')
parser_ntfi.add_argument('--notification-address', help='notificationAddress, if different from URL')
parser_ntfi.add_argument('--iccid', type=is_hexstr, help='ICCID to which the notification relates')
parser_ntfi.add_argument('--smdpp-oid', required=True, help='SM-DP+ OID (as in CERT.DPpb.ECDSA)')
parser_ntfi.add_argument('--isdp-aid', type=is_hexstr, required=True,
help='AID of the ISD-P of the installed profile')
parser_ntfi.add_argument('--sima-response', type=is_hexstr, required=True,
help='hex digits of BER-encoded SAIP EUICCResponse')
class Es9pClient:
def __init__(self, opts):
self.opts = opts
self.cert_and_key = CertAndPrivkey()
self.cert_and_key.cert_from_der_file(os.path.join(opts.certificate_path, opts.euicc_certificate))
self.cert_and_key.privkey_from_pem_file(os.path.join(opts.certificate_path, opts.euicc_private_key))
with open(os.path.join(opts.certificate_path, opts.eum_certificate), 'rb') as f:
self.eum_cert = x509.load_der_x509_certificate(f.read())
with open(os.path.join(opts.certificate_path, opts.ci_certificate), 'rb') as f:
self.ci_cert = x509.load_der_x509_certificate(f.read())
subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), self.ci_cert.extensions))
subject_pkid = subject_exts[0].value
self.ci_pkid = subject_pkid.key_identifier
print("EUICC: %s" % self.cert_and_key.cert.subject)
print("EUM: %s" % self.eum_cert.subject)
print("CI: %s" % self.ci_cert.subject)
self.eid = self.cert_and_key.cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
print("EID: %s" % self.eid)
print("CI PKID: %s" % b2h(self.ci_pkid))
print()
self.peer = es9p.Es9pApiClient(opts.url, server_cert_verify=opts.server_ca_cert)
def do_notification(self):
ntf_metadata = {
'seqNumber': self.opts.sequence_nr,
'profileManagementOperation': PMO(self.opts.operation).to_bitstring(),
'notificationAddress': self.opts.notification_address or urlparse(self.opts.url).netloc,
}
if opts.iccid:
ntf_metadata['iccid'] = h2b(swap_nibbles(opts.iccid))
if self.opts.operation == 'download':
pird = {
'transactionId': self.opts.transaction_id,
'notificationMetadata': ntf_metadata,
'smdpOid': self.opts.smdpp_oid,
'finalResult': ('successResult', {
'aid': self.opts.isdp_aid,
'simaResponse': self.opts.sima_response,
}),
}
pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
signature = self.cert_and_key.ecdsa_sign(pird_bin)
pn_dict = ('profileInstallationResult', {
'profileInstallationResultData': pird,
'euiccSignPIR': signature,
})
else:
ntf_bin = rsp.asn1.encode('NotificationMetadata', ntf_metadata)
signature = self.cert_and_key.ecdsa_sign(ntf_bin)
pn_dict = ('otherSignedNotification', {
'tbsOtherNotification': ntf_metadata,
'euiccNotificationSignature': signature,
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER)),
})
data = {
'pendingNotification': pn_dict,
}
#print(data)
res = self.peer.call_handleNotification(data)
def do_download(self):
print("Step 1: InitiateAuthentication...")
euiccInfo1 = {
'svn': b'\x02\x04\x00',
'euiccCiPKIdListForVerification': [
self.ci_pkid,
],
'euiccCiPKIdListForSigning': [
self.ci_pkid,
],
}
data = {
'euiccChallenge': os.urandom(16),
'euiccInfo1': euiccInfo1,
'smdpAddress': urlparse(self.opts.url).netloc,
}
init_auth_res = self.peer.call_initiateAuthentication(data)
print(init_auth_res)
print("Step 2: AuthenticateClient...")
#res['serverSigned1']
#res['serverSignature1']
print("TODO: verify serverSignature1 over serverSigned1")
#res['transactionId']
print("TODO: verify transactionId matches the signed one in serverSigned1")
#res['euiccCiPKIdToBeUsed']
# TODO: select eUICC certificate based on CI
#res['serverCertificate']
# TODO: verify server certificate against CI
euiccInfo2 = {
'profileVersion': b'\x02\x03\x01',
'svn': euiccInfo1['svn'],
'euiccFirmwareVer': b'\x23\x42\x00',
'extCardResource': b'\x81\x01\x00\x82\x04\x00\x04\x9ch\x83\x02"#',
'uiccCapability': (b'k6\xd3\xc3', 32),
'javacardVersion': b'\x11\x02\x00',
'globalplatformVersion': b'\x02\x03\x00',
'rspCapability': (b'\x9c', 6),
'euiccCiPKIdListForVerification': euiccInfo1['euiccCiPKIdListForVerification'],
'euiccCiPKIdListForSigning': euiccInfo1['euiccCiPKIdListForSigning'],
#'euiccCategory':
#'forbiddenProfilePolicyRules':
'ppVersion': b'\x01\x00\x00',
'sasAcreditationNumber': 'OSMOCOM-TEST-1', #TODO: make configurable
#'certificationDataObject':
}
euiccSigned1 = {
'transactionId': h2b(init_auth_res['transactionId']),
'serverAddress': init_auth_res['serverSigned1']['serverAddress'],
'serverChallenge': init_auth_res['serverSigned1']['serverChallenge'],
'euiccInfo2': euiccInfo2,
'ctxParams1':
('ctxParamsForCommonAuthentication', {
'matchingId': self.opts.matchingId,
'deviceInfo': {
'tac': b'\x35\x23\x01\x45', # same as lpac
'deviceCapabilities': {},
#imei:
}
}),
}
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
euiccSignature1 = self.cert_and_key.ecdsa_sign(euiccSigned1_bin)
auth_clnt_req = {
'transactionId': init_auth_res['transactionId'],
'authenticateServerResponse':
('authenticateResponseOk', {
'euiccSigned1': euiccSigned1,
'euiccSignature1': euiccSignature1,
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER))
})
}
auth_clnt_res = self.peer.call_authenticateClient(auth_clnt_req)
print(auth_clnt_res)
#auth_clnt_res['transactionId']
print("TODO: verify transactionId matches previous ones")
#auth_clnt_res['profileMetadata']
# TODO: what's in here?
#auth_clnt_res['smdpSigned2']['bppEuiccOtpk']
#auth_clnt_res['smdpSignature2']
print("TODO: verify serverSignature2 over smdpSigned2")
smdp_cert = x509.load_der_x509_certificate(auth_clnt_res['smdpCertificate'])
print("Step 3: GetBoundProfilePackage...")
# 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.ECDSA
euicc_ot = ec.generate_private_key(smdp_cert.public_key().public_numbers().curve)
# extract the public key in (hopefully) the right format for the ES8+ interface
euicc_otpk = euicc_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
euiccSigned2 = {
'transactionId': h2b(auth_clnt_res['transactionId']),
'euiccOtpk': euicc_otpk,
#hashCC
}
# check for smdpSigned2 ccRequiredFlag, and send it in PrepareDownloadRequest hashCc
if auth_clnt_res['smdpSigned2']['ccRequiredFlag']:
if not self.opts.confirmation_code:
raise ValueError('Confirmation Code required but not provided')
cc_hash = hashlib.sha256(self.opts.confirmation_code.encode('ascii')).digest()
euiccSigned2['hashCc'] = hashlib.sha256(cc_hash + euiccSigned2['transactionId']).digest()
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
euiccSignature2 = self.cert_and_key.ecdsa_sign(euiccSigned2_bin + auth_clnt_res['smdpSignature2'])
gbp_req = {
'transactionId': auth_clnt_res['transactionId'],
'prepareDownloadResponse':
('downloadResponseOk', {
'euiccSigned2': euiccSigned2,
'euiccSignature2': euiccSignature2,
})
}
gbp_res = self.peer.call_getBoundProfilePackage(gbp_req)
print(gbp_res)
#gbp_res['transactionId']
# TODO: verify transactionId
print("TODO: verify transactionId matches previous ones")
bpp_bin = gbp_res['boundProfilePackage']
print("TODO: verify boundProfilePackage smdpSignature")
bpp = BoundProfilePackage()
upp_bin = bpp.decode(euicc_ot, self.eid, bpp_bin)
iccid = swap_nibbles(b2h(bpp.storeMetadataRequest['iccid']))
base_name = os.path.join(self.opts.output_path, '%s' % iccid)
print("SUCCESS: Storing files as %s.*.der" % base_name)
# write various output files
with open(base_name+'.upp.der', 'wb') as f:
f.write(bpp.upp)
with open(base_name+'.isdp.der', 'wb') as f:
f.write(bpp.encoded_configureISDPRequest)
with open(base_name+'.smr.der', 'wb') as f:
f.write(bpp.encoded_storeMetadataRequest)
if __name__ == '__main__':
opts = parser.parse_args()
c = Es9pClient(opts)
if opts.command == 'download':
c.do_download()
elif opts.command == 'notification':
c.do_notification()
elif opts.command == 'notification-install':
opts.operation = 'install'
c.do_notification()

View File

@@ -1,169 +0,0 @@
#!/usr/bin/env python3
# The purpose of this script is to
# * load two SIM card 'fsdump' files
# * determine which file contents in "B" differs from that of "A"
# * create a pySim-shell script to update the contents of "A" to match that of "B"
# (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 json
import argparse
# Files that we should not update
FILES_TO_SKIP = [
"MF/EF.ICCID",
#"MF/DF.GSM/EF.IMSI",
#"MF/ADF.USIM/EF.IMSI",
]
# Files that need zero-padding at the end, not ff-padding
FILES_PAD_ZERO = [
"DF.GSM/EF.SST",
"MF/ADF.USIM/EF.UST",
"MF/ADF.USIM/EF.EST",
"MF/ADF.ISIM/EF.IST",
]
def pad_file(path, instr, byte_len):
if path in FILES_PAD_ZERO:
pad = '0'
else:
pad = 'f'
return pad_hexstr(instr, byte_len, pad)
def pad_hexstr(instr, byte_len:int, pad='f'):
"""Pad given hex-string to the number of bytes given in byte_len, using ff as padding."""
if len(instr) == byte_len*2:
return instr
elif len(instr) > byte_len*2:
raise ValueError('Cannot pad string of length %u to smaller length %u' % (len(instr)/2, byte_len))
else:
return instr + pad * (byte_len*2 - len(instr))
def is_all_ff(instr):
"""Determine if the entire input hex-string consists of f-digits."""
if all([x == 'f' for x in instr.lower()]):
return True
else:
return False
parser = argparse.ArgumentParser()
parser.add_argument('file_a')
parser.add_argument('file_b')
if __name__ == '__main__':
opts = parser.parse_args()
with open(opts.file_a, 'r') as file_a:
json_a = json.loads(file_a.read())
with open(opts.file_b, 'r') as file_b:
json_b = json.loads(file_b.read())
for path in json_b.keys():
print()
print("# %s" % path)
if not path in json_a:
raise ValueError("%s doesn't exist in file_a!" % path)
if path in FILES_TO_SKIP:
print("# skipped explicitly as it is in FILES_TO_SKIP")
continue
if not 'body' in json_b[path]:
print("# file doesn't exist in B so we cannot possibly need to modify A")
continue
if not 'body' in json_a[path]:
# file was not readable in original (permissions? deactivated?)
print("# ERROR: %s not readable in A; please fix that" % path)
continue
body_a = json_a[path]['body']
body_b = json_b[path]['body']
if body_a == body_b:
print("# file body is identical")
continue
file_size_a = json_a[path]['fcp']['file_size']
file_size_b = json_b[path]['fcp']['file_size']
cmds = []
structure = json_b[path]['fcp']['file_descriptor']['file_descriptor_byte']['structure']
if structure == 'transparent':
val_a = body_a
val_b = body_b
if file_size_a < file_size_b:
if not is_all_ff(val_b[2*file_size_a:]):
print("# ERROR: file_size_a (%u) < file_size_b (%u); please fix!" % (file_size_a, file_size_b))
continue
else:
print("# WARN: file_size_a (%u) < file_size_b (%u); please fix!" % (file_size_a, file_size_b))
# truncate val_b to fit in A
val_b = val_b[:file_size_a*2]
elif file_size_a != file_size_b:
print("# NOTE: file_size_a (%u) != file_size_b (%u)" % (file_size_a, file_size_b))
# Pad to file_size_a
val_b = pad_file(path, val_b, file_size_a)
if val_b != val_a:
cmds.append("update_binary %s" % val_b)
else:
print("# padded file body is identical")
elif structure in ['linear_fixed', 'cyclic']:
record_len_a = json_a[path]['fcp']['file_descriptor']['record_len']
record_len_b = json_b[path]['fcp']['file_descriptor']['record_len']
if record_len_a < record_len_b:
print("# ERROR: record_len_a (%u) < record_len_b (%u); please fix!" % (file_size_a, file_size_b))
continue
elif record_len_a != record_len_b:
print("# NOTE: record_len_a (%u) != record_len_b (%u)" % (record_len_a, record_len_b))
num_rec_a = file_size_a // record_len_a
num_rec_b = file_size_b // record_len_b
if num_rec_a < num_rec_b:
if not all([is_all_ff(x) for x in body_b[num_rec_a:]]):
print("# ERROR: num_rec_a (%u) < num_rec_b (%u); please fix!" % (num_rec_a, num_rec_b))
continue
else:
print("# WARN: num_rec_a (%u) < num_rec_b (%u); but they're empty" % (num_rec_a, num_rec_b))
elif num_rec_a != num_rec_b:
print("# NOTE: num_rec_a (%u) != num_rec_b (%u)" % (num_rec_a, num_rec_b))
i = 0
for r in body_b:
if i < len(body_a):
break
val_a = body_a[i]
# Pad to record_len_a
val_b = pad_file(path, body_b[i], record_len_a)
if val_a != val_b:
cmds.append("update_record %u %s" % (i+1, val_b))
i = i + 1
if len(cmds) == 0:
print("# padded file body is identical")
elif structure == 'ber_tlv':
print("# FIXME: Implement BER-TLV")
else:
raise ValueError('Unsupported structure %s' % structure)
if len(cmds):
print("select %s" % path)
for cmd in cmds:
print(cmd)

View File

@@ -1,98 +1,61 @@
#!/bin/sh -xe
#!/bin/sh
# jenkins build helper script for pysim. This is how we build on jenkins.osmocom.org
#
# 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', 'distcheck', 'pylint', 'docs'
# * SKIP_CLEAN_WORKSPACE: don't run osmo-clean-workspace.sh (for pyosmocom CI)
#
export PYTHONUNBUFFERED=1
set -e
if [ ! -d "./tests/" ] ; then
if [ ! -d "./pysim-testdata/" ] ; then
echo "###############################################"
echo "Please call from pySim-prog top directory"
echo "###############################################"
exit 1
fi
if [ -z "$SKIP_CLEAN_WORKSPACE" ]; then
osmo-clean-workspace.sh
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install pytlv
pip install 'pyyaml>=5.1'
pip install cmd2==1.5
pip install jsonpath-ng
pip install construct
pip install bidict
pip install gsm0338
pip install termcolor
pip install colorlog
pip install pycryptodome
# we need this direct git install, as pypi only lists the python2.7 only release 0.3 from 2013 :(
pip install git+https://github.com/hologram-io/smpp.pdu
# Execute automatically discovered unit tests first
python -m unittest discover -v -s tests/
# Run pylint to find potential errors
# Ignore E1102: not-callable
# pySim/filesystem.py: E1102: method is not callable (not-callable)
# Ignore E0401: import-error
# pySim/utils.py:276: E0401: Unable to import 'Crypto.Cipher' (import-error)
# pySim/utils.py:277: E0401: Unable to import 'Crypto.Util.strxor' (import-error)
pip install pylint
python -m pylint --errors-only \
--disable E1102 \
--disable E0401 \
--enable W0301 \
pySim *.py
# attempt to build documentation
pip install sphinx
pip install sphinxcontrib-napoleon
pip3 install -e 'git+https://github.com/osmocom/sphinx-argparse@master#egg=sphinx-argparse'
(cd docs && make html latexpdf)
if [ "$WITH_MANUALS" = "1" ] && [ "$PUBLISH" = "1" ]; then
make -C "docs" publish publish-html
fi
case "$JOB_TYPE" in
"test")
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install -r requirements.txt
pip install pyshark
# Execute automatically discovered unit tests first
python -m unittest discover -v -s tests/unittests
# Run pySim-prog integration tests (requires physical cards)
cd tests/pySim-prog_test/
./pySim-prog_test.sh
cd ../../
# Run pySim-trace test
tests/pySim-trace_test/pySim-trace_test.sh
# Run pySim-shell integration tests (requires physical cards)
python3 -m unittest discover -v -s ./tests/pySim-shell_test/
;;
"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
pip3 freeze | grep pylint
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install .
# Run pylint to find potential errors
# Ignore E1102: not-callable
# pySim/filesystem.py: E1102: method is not callable (not-callable)
# Ignore E0401: import-error
# pySim/utils.py:276: E0401: Unable to import 'Crypto.Cipher' (import-error)
# pySim/utils.py:277: E0401: Unable to import 'Crypto.Util.strxor' (import-error)
python3 -m pylint -j0 --errors-only \
--disable E1102 \
--disable E0401 \
--enable W0301 \
pySim tests/unittests/*.py *.py \
contrib/*.py
;;
"docs")
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install -r requirements.txt
rm -rf docs/_build
make -C "docs" html latexpdf
if [ "$WITH_MANUALS" = "1" ] && [ "$PUBLISH" = "1" ]; then
make -C "docs" publish publish-html
fi
;;
*)
set +x
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
exit 1
esac
osmo-clean-workspace.sh
# run the test with physical cards
cd pysim-testdata
../tests/pysim-test.sh

View File

@@ -1,258 +0,0 @@
#!/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 os
import sys
import argparse
import logging
import zipfile
from pathlib import Path as PlPath
from typing import List
from osmocom.utils import h2b, b2h, swap_nibbles
from pySim.esim.saip import *
from pySim.esim.saip.validation import CheckBasicStructure
from pySim import javacard
from pySim.pprint import HexBytesPrettyPrinter
pp = HexBytesPrettyPrinter(indent=4,width=500)
parser = argparse.ArgumentParser(description="""
Utility program to work with eSIM SAIP (SimAlliance Interoperable Profile) files.""")
parser.add_argument('INPUT_UPP', help='Unprotected Profile Package Input file')
parser.add_argument("--loglevel", dest="loglevel", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
default='INFO', help="Set the logging level")
parser.add_argument('--debug', action='store_true', help='Enable DEBUG logging')
subparsers = parser.add_subparsers(dest='command', help="The command to perform", required=True)
parser_split = subparsers.add_parser('split', help='Split PE-Sequence into individual PEs')
parser_split.add_argument('--output-prefix', default='.', help='Prefix path/filename for output files')
parser_dump = subparsers.add_parser('dump', help='Dump information on PE-Sequence')
parser_dump.add_argument('mode', choices=['all_pe', 'all_pe_by_type', 'all_pe_by_naa'])
parser_dump.add_argument('--dump-decoded', action='store_true', help='Dump decoded PEs')
parser_check = subparsers.add_parser('check', help='Run constraint checkers on PE-Sequence')
parser_rpe = subparsers.add_parser('remove-pe', help='Remove specified PEs from PE-Sequence')
parser_rpe.add_argument('--output-file', required=True, help='Output file name')
parser_rpe.add_argument('--identification', type=int, action='append', help='Remove PEs matching specified identification')
parser_rn = subparsers.add_parser('remove-naa', help='Remove speciifed NAAs from PE-Sequence')
parser_rn.add_argument('--output-file', required=True, help='Output file name')
parser_rn.add_argument('--naa-type', required=True, choices=NAAs.keys(), help='Network Access Application type to remove')
# TODO: add an --naa-index or the like, so only one given instance can be removed
parser_info = subparsers.add_parser('info', help='Display information about the profile')
parser_eapp = subparsers.add_parser('extract-apps', help='Extract applications as loadblock file')
parser_eapp.add_argument('--output-dir', default='.', help='Output directory (where to store files)')
parser_eapp.add_argument('--format', default='cap', choices=['ijc', 'cap'], help='Data format of output files')
parser_info = subparsers.add_parser('tree', help='Display the filesystem tree')
def do_split(pes: ProfileElementSequence, opts):
i = 0
for pe in pes.pe_list:
basename = PlPath(opts.INPUT_UPP).stem
if not pe.identification:
fname = '%s-%02u-%s.der' % (basename, i, pe.type)
else:
fname = '%s-%02u-%05u-%s.der' % (basename, i, pe.identification, pe.type)
print("writing single PE to file '%s'" % fname)
with open(os.path.join(opts.output_prefix, fname), 'wb') as outf:
outf.write(pe.to_der())
i += 1
def do_dump(pes: ProfileElementSequence, opts):
def print_all_pe(pes: ProfileElementSequence, dump_decoded:bool = False):
# iterate over each pe in the pes (using its __iter__ method)
for pe in pes:
print("="*70 + " " + pe.type)
if dump_decoded:
pp.pprint(pe.decoded)
def print_all_pe_by_type(pes: ProfileElementSequence, dump_decoded:bool = False):
# sort by PE type and show all PE within that type
for pe_type in pes.pe_by_type.keys():
print("="*70 + " " + pe_type)
for pe in pes.pe_by_type[pe_type]:
pp.pprint(pe)
if dump_decoded:
pp.pprint(pe.decoded)
def print_all_pe_by_naa(pes: ProfileElementSequence, dump_decoded:bool = False):
for naa in pes.pes_by_naa:
i = 0
for naa_instance in pes.pes_by_naa[naa]:
print("="*70 + " " + naa + str(i))
i += 1
for pe in naa_instance:
pp.pprint(pe.type)
if dump_decoded:
for d in pe.decoded:
print(" %s" % d)
if opts.mode == 'all_pe':
print_all_pe(pes, opts.dump_decoded)
elif opts.mode == 'all_pe_by_type':
print_all_pe_by_type(pes, opts.dump_decoded)
elif opts.mode == 'all_pe_by_naa':
print_all_pe_by_naa(pes, opts.dump_decoded)
def do_check(pes: ProfileElementSequence, opts):
print("Checking PE-Sequence structure...")
checker = CheckBasicStructure()
checker.check(pes)
print("All good!")
def do_remove_pe(pes: ProfileElementSequence, opts):
new_pe_list = []
for pe in pes.pe_list:
identification = pe.identification
if identification:
if identification in opts.identification:
print("Removing PE %s (id=%u) from Sequence..." % (pe, identification))
continue
new_pe_list.append(pe)
pes.pe_list = new_pe_list
pes._process_pelist()
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
with open(opts.output_file, 'wb') as f:
f.write(pes.to_der())
def do_remove_naa(pes: ProfileElementSequence, opts):
if not opts.naa_type in NAAs:
raise ValueError('unsupported NAA type %s' % opts.naa_type)
naa = NAAs[opts.naa_type]
print("Removing NAAs of type '%s' from Sequence..." % opts.naa_type)
pes.remove_naas_of_type(naa)
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
with open(opts.output_file, 'wb') as f:
f.write(pes.to_der())
def do_info(pes: ProfileElementSequence, opts):
def get_naa_count(pes: ProfileElementSequence) -> dict:
"""return a dict with naa-type (usim, isim) as key and the count of NAA instances as value."""
ret = {}
for naa_type in pes.pes_by_naa:
ret[naa_type] = len(pes.pes_by_naa[naa_type])
return ret
pe_hdr_dec = pes.pe_by_type['header'][0].decoded
print()
print("SAIP Profile Version: %u.%u" % (pe_hdr_dec['major-version'], pe_hdr_dec['minor-version']))
print("Profile Type: '%s'" % pe_hdr_dec['profileType'])
print("ICCID: %s" % b2h(pe_hdr_dec['iccid']))
print("Mandatory Services: %s" % ', '.join(pe_hdr_dec['eUICC-Mandatory-services'].keys()))
print()
naa_strs = ["%s[%u]" % (k, v) for k, v in get_naa_count(pes).items()]
print("NAAs: %s" % ', '.join(naa_strs))
for naa_type in pes.pes_by_naa:
for naa_inst in pes.pes_by_naa[naa_type]:
first_pe = naa_inst[0]
adf_name = ''
if hasattr(first_pe, 'adf_name'):
adf_name = '(' + first_pe.adf_name + ')'
print("NAA %s %s" % (first_pe.type, adf_name))
if hasattr(first_pe, 'imsi'):
print("\tIMSI: %s" % first_pe.imsi)
# applications
print()
apps = pes.pe_by_type.get('application', [])
print("Number of applications: %u" % len(apps))
for app_pe in apps:
print("App Load Package AID: %s" % b2h(app_pe.decoded['loadBlock']['loadPackageAID']))
print("\tMandated: %s" % ('mandated' in app_pe.decoded['app-Header']))
print("\tLoad Block Size: %s" % len(app_pe.decoded['loadBlock']['loadBlockObject']))
for inst in app_pe.decoded.get('instanceList', []):
print("\tInstance AID: %s" % b2h(inst['instanceAID']))
# security domains
print()
sds = pes.pe_by_type.get('securityDomain', [])
print("Number of security domains: %u" % len(sds))
for sd in sds:
print("Security domain Instance AID: %s" % b2h(sd.decoded['instance']['instanceAID']))
# FIXME: 'applicationSpecificParametersC9' parsing to figure out enabled SCP
for key in sd.keys:
print("\tKVN=0x%02x, KID=0x%02x, %s" % (key.key_version_number, key.key_identifier, key.key_components))
# RFM
print()
rfms = pes.pe_by_type.get('rfm', [])
print("Number of RFM instances: %u" % len(rfms))
for rfm in rfms:
inst_aid = rfm.decoded['instanceAID']
print("RFM instanceAID: %s" % b2h(inst_aid))
print("\tMSL: 0x%02x" % rfm.decoded['minimumSecurityLevel'][0])
adf = rfm.decoded.get('adfRFMAccess', None)
if adf:
print("\tADF AID: %s" % b2h(adf['adfAID']))
tar_list = rfm.decoded.get('tarList', [inst_aid[-3:]])
for tar in tar_list:
print("\tTAR: %s" % b2h(tar))
def do_extract_apps(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
fname = os.path.join(opts.output_dir, '%s-%s.%s' % (pes.iccid, package_aid, opts.format))
load_block_obj = app_pe.decoded['loadBlock']['loadBlockObject']
print("Writing Load Package AID: %s to file %s" % (package_aid, fname))
if opts.format == 'ijc':
with open(fname, 'wb') as f:
f.write(load_block_obj)
else:
with io.BytesIO(load_block_obj) as f, zipfile.ZipFile(fname, 'w') as z:
javacard.ijc_to_cap(f, z, package_aid)
def do_tree(pes:ProfileElementSequence, opts):
pes.mf.print_tree()
if __name__ == '__main__':
opts = parser.parse_args()
if opts.debug:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.getLevelName(opts.loglevel))
with open(opts.INPUT_UPP, 'rb') as f:
pes = ProfileElementSequence.from_der(f.read())
print("Read %u PEs from file '%s'" % (len(pes.pe_list), opts.INPUT_UPP))
if opts.command == 'split':
do_split(pes, opts)
elif opts.command == 'dump':
do_dump(pes, opts)
elif opts.command == 'check':
do_check(pes, opts)
elif opts.command == 'remove-pe':
do_remove_pe(pes, opts)
elif opts.command == 'remove-naa':
do_remove_naa(pes, opts)
elif opts.command == 'info':
do_info(pes, opts)
elif opts.command == 'extract-apps':
do_extract_apps(pes, opts)
elif opts.command == 'tree':
do_tree(pes, opts)

View File

@@ -162,7 +162,6 @@ 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)

View File

@@ -26,10 +26,7 @@ from klein import Klein
from pySim.transport import ApduTracer
from pySim.transport.pcsc import PcscSimLink
from pySim.commands import SimCardCommands
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.cards import UsimCard
from pySim.exceptions import *
class ApduPrintTracer(ApduTracer):
@@ -38,20 +35,18 @@ class ApduPrintTracer(ApduTracer):
pass
def connect_to_card(slot_nr:int):
tp = PcscSimLink(argparse.Namespace(pcsc_dev=slot_nr), apdu_tracer=ApduPrintTracer())
tp = PcscSimLink(slot_nr, apdu_tracer=ApduPrintTracer())
tp.connect()
scc = SimCardCommands(tp)
card = UiccCardBase(scc)
card = UsimCard(scc)
# this should be part of UsimCard, but FairewavesSIM breaks with that :/
scc.cla_byte = "00"
scc.sel_ctrl = "0004"
card.read_aids()
# ensure that MF is selected when we are done.
card._scc.select_file('3f00')
card.select_adf_by_aid(adf='usim')
return tp, scc, card
@@ -101,7 +96,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>')
@@ -137,14 +132,10 @@ 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')
ef_imsi = EF_IMSI()
(imsi, sw) = card._scc.read_binary(ef_imsi.fid)
res = {"imsi": dec_imsi(imsi), "iccid": dec_iccid(iccid) }
iccid, sw = card.read_iccid()
imsi, sw = card.read_imsi()
res = {"imsi": imsi, "iccid": iccid }
tp.disconnect()

View File

@@ -1,43 +0,0 @@
#!/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 osmocom.utils import b2h, h2b
from osmocom.tlv import bertlv_parse_one, bertlv_encode_tag
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)
else:
# avoid pylint "(possibly-used-before-assignment)" below
sys.exit(2)
process_one_level(content, 0)

View File

@@ -1,112 +0,0 @@
#!/usr/bin/env python3
"""Connect smartcard to a remote server, so the remote server can take control and
perform commands on it."""
import sys
import json
import logging
import argparse
from osmocom.utils import b2h
import websockets
from pySim.transport import init_reader, argparse_add_reader_args, LinkBase
from pySim.commands import SimCardCommands
from pySim.wsrc.client_blocking import WsClientBlocking
from pySim.exceptions import NoCardError
from pySim.wsrc import WSRC_DEFAULT_PORT_CARD
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
class CardWsClientBlocking(WsClientBlocking):
"""Implementation of the card (reader) client of the WSRC (WebSocket Remote Card) protocol"""
def __init__(self, ws_uri, tp: LinkBase):
super().__init__('card', ws_uri)
self.tp = tp
def perform_outbound_hello(self):
hello_data = {
'atr': b2h(self.tp.get_atr()),
# TODO: include various card information in the HELLO message
}
super().perform_outbound_hello(hello_data)
def handle_rx_c_apdu(self, rx: dict):
"""handle an inbound APDU transceive command"""
data, sw = self.tp.send_apdu(rx['command'])
tx = {
'response': data,
'sw': sw,
}
self.tx_json('r_apdu', tx)
def handle_rx_disconnect(self, rx: dict):
"""server tells us to disconnect"""
self.tx_json('disconnect_ack')
# FIXME: tear down connection and/or terminate entire program
def handle_rx_state_notification(self, rx: dict):
logger.info("State Notification: %s" % rx['new_state'])
def handle_rx_print(self, rx: dict):
"""print a message (text) given by server to the local console/log"""
logger.info("SERVER MSG: %s" % rx['message'])
# no response
def handle_rx_reset_req(self, rx: dict):
"""server tells us to reset the card"""
self.tp.reset_card()
self.tx_json('reset_resp', {'atr': b2h(self.tp.get_atr())})
parser = argparse.ArgumentParser()
argparse_add_reader_args(parser)
parser.add_argument("--uri", default="ws://localhost:%u/" % (WSRC_DEFAULT_PORT_CARD),
help="URI of the sever to which to connect")
if __name__ == '__main__':
opts = parser.parse_args()
# open the card reader / slot
logger.info("Initializing Card Reader...")
try:
tp = init_reader(opts)
except Exception as e:
logger.fatal("Error opening reader: %s" % e)
sys.exit(1)
logger.info("Connecting to Card...")
try:
tp.connect()
except NoCardError as e:
logger.fatal("Error opening card! Is a card inserted in the reader?")
sys.exit(1)
scc = SimCardCommands(transport=tp)
logger.info("Detected Card with ATR: %s" % b2h(tp.get_atr()))
# TODO: gather various information about the card; print it
# create + connect the client to the server
cl = CardWsClientBlocking(opts.uri, tp)
logger.info("Connecting to remote server...")
try:
cl.connect()
logger.info("Successfully connected to Server")
except ConnectionRefusedError as e:
logger.fatal(e)
sys.exit(1)
try:
while True:
# endless loop: wait for inbound command from server + execute it
cl.rx_and_execute_cmd()
except websockets.exceptions.ConnectionClosedOK as e:
print(e)
sys.exit(1)
except KeyboardInterrupt as e:
print(e.__class__.__name__)
sys.exit(2)

View File

@@ -1,354 +0,0 @@
#!/usr/bin/env python
import json
import asyncio
import logging
import argparse
from typing import Optional, Tuple
from websockets.asyncio.server import serve
from websockets.exceptions import ConnectionClosedError
from osmocom.utils import Hexstr, swap_nibbles
from pySim.utils import SwMatchstr, ResTuple, sw_match, dec_iccid
from pySim.exceptions import SwMatchError
from pySim.wsrc import WSRC_DEFAULT_PORT_USER, WSRC_DEFAULT_PORT_CARD
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
card_clients = set()
user_clients = set()
class WsClientLogAdapter(logging.LoggerAdapter):
"""LoggerAdapter adding context (for now: remote IP/Port of client) to log"""
def process(self, msg, kwargs):
return '%s([%s]:%u) %s' % (self.extra['type'], self.extra['remote_addr'][0],
self.extra['remote_addr'][1], msg), kwargs
class WsClient:
def __init__(self, websocket, hello: dict):
self.websocket = websocket
self.hello = hello
self.identity = {}
self.logger = WsClientLogAdapter(logger, {'type': self.__class__.__name__,
'remote_addr': websocket.remote_address})
def __str__(self):
return '%s([%s]:%u)' % (self.__class__.__name__, self.websocket.remote_address[0],
self.websocket.remote_address[1])
async def rx_json(self):
rx = await self.websocket.recv()
rx_js = json.loads(rx)
self.logger.debug("Rx: %s", rx_js)
assert 'msg_type' in rx
return rx_js
async def tx_json(self, msg_type:str, d: dict = {}):
"""Transmit a json-serializable dict to the peer"""
d['msg_type'] = msg_type
d_js = json.dumps(d)
self.logger.debug("Tx: %s", d_js)
await self.websocket.send(d_js)
async def tx_hello_ack(self):
await self.tx_json('hello_ack')
async def xceive_json(self, msg_type:str, d:dict = {}, exp_msg_type:Optional[str] = None) -> dict:
await self.tx_json(msg_type, d)
rx = await self.rx_json()
if exp_msg_type:
assert rx['msg_type'] == exp_msg_type
return rx;
async def tx_error(self, message: str):
"""Transmit an error message to the peer"""
event = {
"message": message,
}
self.logger.error("Transmitting error message: '%s'" % message)
await self.tx_json('error', event)
# async def ws_hdlr(self):
# """kind of a 'main' function for the websocket client: wait for incoming message,
# and handle it."""
# try:
# async for message in self.websocket:
# method = getattr(self, 'handle_rx_%s' % message['msg_type'], None)
# if not method:
# await self.tx_error("Unknonw msg_type: %s" % message['msg_type'])
# else:
# method(message)
# except ConnectionClosedError:
# # we handle this in the outer loop
# pass
class CardClient(WsClient):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.identity['ATR'] = self.hello['atr']
self.state = 'init'
def __str__(self):
eid = self.identity.get('EID', None)
if eid:
return '%s(EID=%s)' % (self.__class__.__name__, eid)
iccid = self.identity.get('ICCID', None)
if iccid:
return '%s(ICCID=%s)' % (self.__class__.__name__, iccid)
return super().__str__()
def listing_entry(self) -> dict:
return {'remote_addr': self.websocket.remote_address,
'identities': self.identity,
'state': self.state}
"""A websocket client that represents a reader/card. This is what we use to talk to a card"""
async def xceive_apdu_raw(self, cmd: Hexstr) -> ResTuple:
"""transceive a single APDU with the card"""
message = await self.xceive_json('c_apdu', {'command': cmd}, 'r_apdu')
return message['response'], message['sw']
async def xceive_apdu(self, pdu: Hexstr) -> ResTuple:
"""transceive an APDU with the card, handling T=0 GET_RESPONSE cases"""
prev_pdu = pdu
data, sw = await self.xceive_apdu_raw(pdu)
if sw is not None:
while (sw[0:2] in ['9f', '61', '62', '63']):
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
prev_pdu = pdu_gr
d, sw = await self.xceive_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 = prev_pdu[0:8] + sw[2:4]
data, sw = await self.xceive_apdu_raw(pdu_gr)
return data, sw
async def xceive_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""like xceive_apdu, but checking the status word matches the expected pattern"""
rv = await self.xceive_apdu(pdu)
last_sw = rv[1]
if not sw_match(rv[1], sw):
raise SwMatchError(rv[1], sw.lower())
return rv
async def card_reset(self):
"""reset the card"""
rx = await self.xceive_json('reset_req', exp_msg_type='reset_resp')
self.identity['ATR'] = rx['atr']
async def notify_state(self):
"""notify the card client of a state [change]"""
await self.tx_json('state_notification', {'new_state': self.state})
async def get_iccid(self):
"""high-level method to obtain the ICCID of the card"""
await self.xceive_apdu_checksw('00a40000023f00') # SELECT MF
await self.xceive_apdu_checksw('00a40000022fe2') # SELECT EF.ICCID
res, sw = await self.xceive_apdu_checksw('00b0000000') # READ BINARY
return dec_iccid(res)
async def get_eid_sgp22(self):
"""high-level method to obtain the EID of a SGP.22 consumer eUICC"""
await self.xceive_apdu_checksw('00a4040410a0000005591010ffffffff8900000100')
res, sw = await self.xceive_apdu_checksw('80e2910006bf3e035c015a')
return res[-32:]
async def set_state(self, new_state:str):
self.logger.info("Card now in '%s' state" % new_state)
self.state = new_state
await self.notify_state()
async def identify(self):
# identify the card by asking for its EID and/or ICCID
try:
eid = await self.get_eid_sgp22()
self.logger.debug("EID: %s", eid)
self.identity['EID'] = eid
except SwMatchError:
pass
try:
iccid = await self.get_iccid()
self.logger.debug("ICCID: %s", iccid)
self.identity['ICCID'] = iccid
except SwMatchError:
pass
await self.set_state('ready')
async def associate_user(self, user):
assert self.state == 'ready'
self.user = user
await self.set_state('associated')
async def disassociate_user(self):
assert self.user
assert self.state == 'associated'
await self.set_state('ready')
@staticmethod
def find_client_for_id(id_type: str, id_str: str) -> Optional['CardClient']:
for c in card_clients:
if c.state != 'ready':
continue
c_id = c.identity.get(id_type.upper(), None)
if c_id and c_id.lower() == id_str.lower():
return c
return None
class UserClient(WsClient):
"""A websocket client representing a user application like pySim-shell."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.state = 'init'
self.card = None
async def associate_card(self, card: CardClient):
assert self.state == 'init'
self.card = card
self.state = 'associated'
async def disassociate_card(self):
assert self.state == 'associated'
self.card = None
self.state = 'init'
async def state_init(self):
"""Wait for incoming 'select_card' and process it."""
while True:
rx = await self.rx_json()
if rx['msg_type'] == 'select_card':
# look-up if the card can be found
card = CardClient.find_client_for_id(rx['id_type'], rx['id_str'])
if not card:
await self.tx_error('No CardClient found for %s == %s' % (rx['id_type'], rx['id_str']))
continue
# transition to next statee
await card.associate_user(self)
await self.associate_card(card)
await self.tx_json('select_card_ack', {'identities': card.identity})
break
elif rx['msg_type'] == 'list_cards':
res = [x.listing_entry() for x in card_clients]
await self.tx_json('list_cards_ack', {'cards': res})
else:
self.tx_error('Unknown message type %s' % rx['msg_type'])
async def state_selected(self):
while True:
rx = await self.rx_json()
if rx['msg_type'] == 'c_apdu':
rsp, sw = await self.card.xceive_apdu_raw(rx['command'])
await self.tx_json('r_apdu', {'response': rsp, 'sw': sw})
elif rx['msg_type'] == 'reset_req':
await self.card.card_reset()
await self.tx_json('reset_resp')
else:
self.logger.warning("Unknown/unsupported command '%s' received" % rx['msg_type'])
async def card_conn_hdlr(websocket):
"""Handler for incoming connection to 'card' port."""
# receive first message, which should be a 'hello'
rx_raw = await websocket.recv()
rx = json.loads(rx_raw)
assert rx['msg_type'] == 'hello'
client_type = rx['client_type']
if client_type != 'card':
logger.error("Rejecting client (unknown type %s) connection", client_type)
raise ValueError("client_type '%s' != expected 'card'" % client_type)
card = CardClient(websocket, rx)
card.logger.info("connection accepted")
# first go through identity phase
async with asyncio.timeout(30):
await card.tx_hello_ack()
card.logger.info("hello-handshake completed")
card_clients.add(card)
# first obtain the identity of the card
await card.identify()
# then go into the "main loop"
try:
# wait 'indefinitely'. We cannot call websocket.recv() here, as we will call another
# recv() while waiting for the R-APDU after forwarding one from the user client.
await websocket.wait_closed()
finally:
card.logger.info("connection closed")
card_clients.remove(card)
async def user_conn_hdlr(websocket):
"""Handler for incoming connection to 'user' port."""
# receive first message, which should be a 'hello'
rx_raw = await websocket.recv()
rx = json.loads(rx_raw)
assert rx['msg_type'] == 'hello'
client_type = rx['client_type']
if client_type != 'user':
logger.error("Rejecting client (unknown type %s) connection", client_type)
raise ValueError("client_type '%s' != expected 'card'" % client_type)
user = UserClient(websocket, rx)
user.logger.info("connection accepted")
# first go through hello phase
async with asyncio.timeout(10):
await user.tx_hello_ack()
user.logger.info("hello-handshake completed")
user_clients.add(user)
# first wait for the user to specify the select the card
try:
await user.state_init()
except ConnectionClosedError:
user.logger.info("connection closed")
user_clients.remove(user)
return
except asyncio.exceptions.CancelledError: # direct cause of TimeoutError
user.logger.error("User failed to transition to 'associated' state within 10s")
user_clients.remove(user)
return
except Exception as e:
user.logger.error("Unknown exception %s", e)
raise e
user_clients.remove(user)
return
# then perform APDU exchanges without any time limit
try:
await user.state_selected()
except ConnectionClosedError:
pass
finally:
user.logger.info("connection closed")
await user.card.disassociate_user()
await user.disassociate_card()
user_clients.remove(user)
parser = argparse.ArgumentParser(description="""Osmocom WebSocket Remote Card server""")
parser.add_argument('--user-bind-port', type=int, default=WSRC_DEFAULT_PORT_USER,
help="port number to bind for connections from users")
parser.add_argument('--user-bind-host', type=str, default='localhost',
help="local Host/IP to bind for connections from users")
parser.add_argument('--card-bind-port', type=int, default=WSRC_DEFAULT_PORT_CARD,
help="port number to bind for connections from cards")
parser.add_argument('--card-bind-host', type=str, default='localhost',
help="local Host/IP to bind for connections from cards")
if __name__ == '__main__':
opts = parser.parse_args()
async def main():
# we use different ports for user + card connections to ensure they can
# have different packet filter rules apply to them
async with serve(card_conn_hdlr, opts.card_bind_host, opts.card_bind_port), serve(user_conn_hdlr, opts.user_bind_host, opts.user_bind_port):
await asyncio.get_running_loop().create_future() # run forever
asyncio.run(main())

View File

@@ -4,22 +4,16 @@
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= python3 -m sphinx.cmd.build
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# for osmo-gsm-manuals
OSMO_GSM_MANUALS_DIR ?= $(shell pkg-config osmo-gsm-manuals --variable=osmogsmmanualsdir 2>/dev/null)
OSMO_GSM_MANUALS_DIR=$(shell pkg-config osmo-gsm-manuals --variable=osmogsmmanualsdir 2>/dev/null)
OSMO_REPOSITORY = "pysim"
UPLOAD_FILES = $(BUILDDIR)/latex/osmopysim-usermanual.pdf
CLEAN_FILES = $(UPLOAD_FILES)
# Copy variables from Makefile.common.inc that are used in publish-html,
# as Makefile.common.inc must be included after publish-html
PUBLISH_REF ?= master
PUBLISH_TEMPDIR = _publish_tmpdir
SSH_COMMAND = ssh -o 'UserKnownHostsFile=$(OSMO_GSM_MANUALS_DIR)/build/known_hosts' -p 48
# Put it first so that "make" without argument is like "make help".
.PHONY: help
help:
@@ -29,16 +23,7 @@ $(BUILDDIR)/latex/pysim.pdf: latexpdf
@/bin/true
publish-html: html
rm -rf "$(PUBLISH_TEMPDIR)"
mkdir -p "$(PUBLISH_TEMPDIR)/pysim/$(PUBLISH_REF)"
cp -r "$(BUILDDIR)"/html "$(PUBLISH_TEMPDIR)/pysim/$(PUBLISH_REF)"
cd "$(PUBLISH_TEMPDIR)" && \
rsync \
-avzR \
-e "$(SSH_COMMAND)" \
"pysim" \
docs@ftp.osmocom.org:web-files/
rm -rf "$(PUBLISH_TEMPDIR)"
rsync -avz -e "ssh -o 'UserKnownHostsFile=$(OSMO_GSM_MANUALS_DIR)/build/known_hosts' -p 48" $(BUILDDIR)/html/ docs@ftp.osmocom.org:web-files/latest/pysim/
# put this before the catch-all below
include $(OSMO_GSM_MANUALS_DIR)/build/Makefile.common.inc
@@ -47,6 +32,4 @@ include $(OSMO_GSM_MANUALS_DIR)/build/Makefile.common.inc
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%:
@if [ "$@" != "shrink" ]; then \
$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O); \
fi
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -1,126 +0,0 @@
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.

View File

@@ -18,8 +18,8 @@ sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = 'osmopysim-usermanual'
copyright = '2009-2023 by Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle, Merlin Chlosta'
author = 'Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle, Merlin Chlosta'
copyright = '2009-2022 by Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle'
author = 'Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle'
# -- General configuration ---------------------------------------------------

View File

@@ -31,7 +31,6 @@ pySim consists of several parts:
* a python :ref:`library<pySim library>` containing plenty of objects and methods that can be used for
writing custom programs interfacing with SIM cards.
* the [new] :ref:`interactive pySim-shell command line program<pySim-shell>`
* the [new] :ref:`pySim-trace APDU trace decoder<pySim-trace>`
* the [legacy] :ref:`pySim-prog and pySim-read tools<Legacy tools>`
.. toctree::
@@ -39,11 +38,8 @@ pySim consists of several parts:
:caption: Contents:
shell
trace
legacy
library
osmo-smdpp
wsrc
Indices and tables

View File

@@ -1,20 +1,20 @@
Legacy tools
Legacy tools
============
*legacy tools* are the classic ``pySim-prog`` and ``pySim-read`` programs that
existed long before ``pySim-shell``.
These days, it is highly recommended to use ``pySim-shell`` instead of these
These days, you should primarily use ``pySim-shell`` instead of these
legacy tools.
pySim-prog
----------
``pySim-prog`` was the first part of the pySim software suite. It started as a
tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and was
later extended to a variety of other cards. As the number of features supported
became no longer bearable to express with command-line arguments, `pySim-shell`
was created.
``pySim-prog`` was the first part of the pySim software suite. It started as
a tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and
was later extended to a variety of other cards. As the number of features supported
became no longer bearable to express with command-line arguments, `pySim-shell` was
created.
Basic use cases can still use `pySim-prog`.
@@ -22,180 +22,36 @@ Program customizable SIMs
~~~~~~~~~~~~~~~~~~~~~~~~~
Two modes are possible:
- one where the user specifies every parameter manually:
- one where you specify every parameter manually :
This is the most common way to use ``pySim-prog``. The user will specify all relevant parameters directly via the
commandline. A typical commandline would look like this:
``pySim-prog.py -p <pcsc_reader> --ki <ki_value> --opc <opc_value> --mcc <mcc_value> --mnc <mnc_value>
--country <country_code> --imsi <imsi_value> --iccid <iccid_value> --pin-adm <adm_pin>``
Please note, that this already lengthy commandline still only contains the most common card parameters. For a full
list of all possible parameters, use the ``--help`` option of ``pySim-prog``. It is also important to mention
that not all parameters are supported by all card types. In particular, very simple programmable SIM cards will only
support a very basic set of parameters, such as MCC, MNC, IMSI and KI values.
- one where the parameters are generated from a minimal set:
It is also possible to leave the generation of certain parameters to ``pySim-prog``. This is in particular helpful
when a large number of cards should be initialized with randomly generated key material.
``pySim-prog.py -p <pcsc_reader> --mcc <mcc_value> --mnc <mnc_value> --secret <random_secret> --num <card_number> --pin-adm <adm_pin>``
The parameter ``--secret`` specifies a random seed that is used to generate the card individual parameters. (IMSI).
The secret should contain enough randomness to avoid conflicts. It is also recommended to store the secret safely,
in case cards have to be re-generated or the current card batch has to be extended later. For security reasons, the
key material, which is also card individual, will not be derived from the random seed. Instead a new random set of
Ki and OPc will be generated during each programming cycle. This means fresh keys are generated, even when the
``--num`` remains unchanged.
The parameter ``--num`` specifies a card individual number. This number will be manged into the random seed so that
it serves as an identifier for a particular set of randomly generated parameters.
In the example above the parameters ``--mcc``, and ``--mnc`` are specified as well, since they identify the GSM
network where the cards should operate in, it is absolutely required to keep them static. ``pySim-prog`` will use
those parameters to generate a valid IMSI that thas the specified MCC/MNC at the beginning and a random tail.
Specifying the card type:
``pySim-prog`` usually autodetects the card type. In case auto detection does not work, it is possible to specify
the parameter ``--type``. The following card types are supported:
* Fairwaves-SIM
* fakemagicsim
* gialersim
* grcardsim
* magicsim
* OpenCells-SIM
* supersim
* sysmoISIM-SJA2
* sysmoISIM-SJA5
* sysmosim-gr1
* sysmoSIM-GR2
* sysmoUSIM-SJS1
* Wavemobile-SIM
Specifying the card reader:
It is most common to use ``pySim-prog`` together whith a PCSC reader. The PCSC reader number is specified via the
``--pcsc-device`` or ``-p`` option. However, other reader types (such as serial readers and modems) are supported. Use
the ``--help`` option of ``pySim-prog`` for more information.
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -i <IMSI> -s <ICCID>``
Card programming using CSV files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- one where they are generated from some minimal set :
To simplify the card programming process, ``pySim-prog`` also allows to read
the card parameters from a CSV file. When a CSV file is used as input, the
user does not have to craft an individual commandline for each card. Instead
all card related parameters are automatically drawn from the CSV file.
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -z <random_string_of_choice> -j <card_num>``
A CSV files may hold rows for multiple (hundreds or even thousands) of
cards. ``pySim-prog`` is able to identify the rows either by ICCID
(recommended as ICCIDs are normally not changed) or IMSI.
With <random_string_of_choice> and <card_num>, the soft will generate
'predictable' IMSI and ICCID, so make sure you choose them so as not to
conflict with anyone. (for eg. your name as <random_string_of_choice> and
0 1 2 ... for <card num>).
The CSV file format is a flexible format with mandatory and optional columns,
here the same rules as for the commandline parameters apply. The column names
match the command line options. The CSV file may also contain columns that are
unknown to pySim-prog, such as inventory numbers, nicknames or parameters that
are unrelated to the card programming process. ``pySim-prog`` will silently
ignore all unknown columns.
A CSV file may contain the following columns:
* name
* iccid (typically used as key)
* mcc
* mnc
* imsi (may be used as key, but not recommended)
* smsp
* ki
* opc
* acc
* pin_adm, adm1 or pin_adm_hex (must be present)
* msisdn
* epdgid
* epdgSelection
* pcscf
* ims_hdomain
* impi
* impu
* opmode
* fplmn
Due to historical reasons, and to maintain the compatibility between multiple different CSV file formats, the ADM pin
may be stored in three different columns. Only one of the three columns must be available.
* adm1: This column contains the ADM pin in numeric ASCII digit format. This format is the most common.
* pin_adm: Same as adm1, only the column name is different
* pin_adm_hex: If the ADM pin consists of raw HEX digits, rather then of numerical ASCII digits, then the ADM pin
can also be provided as HEX string using this column.
The following example shows a typical minimal example
::
"imsi","iccid","acc","ki","opc","adm1"
"999700000053010","8988211000000530108","0001","51ACE8BD6313C230F0BFE1A458928DF0","E5A00E8DE427E21B206526B5D1B902DF","65942330"
"999700000053011","8988211000000530116","0002","746AAFD7F13CFED3AE626B770E53E860","38F7CE8322D2A7417E0BBD1D7B1190EC","13445792"
"999700123053012","8988211000000530124","0004","D0DA4B7B150026ADC966DC637B26429C","144FD3AEAC208DFFF4E2140859BAE8EC","53540383"
"999700000053013","8988211000000530132","0008","52E59240ABAC6F53FF5778715C5CE70E","D9C988550DC70B95F40342298EB84C5E","26151368"
"999700000053014","8988211000000530140","0010","3B4B83CB9C5F3A0B41EBD17E7D96F324","D61DCC160E3B91F284979552CC5B4D9F","64088605"
"999700000053015","8988211000000530157","0020","D673DAB320D81039B025263610C2BBB3","4BCE1458936B338067989A06E5327139","94108841"
"999700000053016","8988211000000530165","0040","89DE5ACB76E06D14B0F5D5CD3594E2B1","411C4B8273FD7607E1885E59F0831906","55184287"
"999700000053017","8988211000000530173","0080","977852F7CEE83233F02E69E211626DE1","2EC35D48DBF2A99C07D4361F19EF338F","70284674"
The following commandline will instruct ``pySim-prog`` to use the provided CSV file as parameter source and the
ICCID (read from the card before programming) as a key to identify the card. To use the IMSI as a key, the parameter
``--read-imsi`` can be used instead of ``--read-iccid``. However, this option is only recommended to be used in very
specific corner cases.
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_csv_file> --source csv --read-iccid``
It is also possible to pick a row from the CSV file by manually providing an ICCID (option ``--iccid``) or an IMSI
(option ``--imsi``) that is then used as a key to find the matching row in the CSV file.
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_csv_file> --source csv --iccid <iccid_value>``
Writing CSV files
~~~~~~~~~~~~~~~~~
``pySim-prog`` is also able to generate CSV files that contain a subset of the parameters it has generated or received
from some other source (commandline, CSV-File). The generated file will be header-less and contain the following
columns:
* name
* iccid
* mcc
* mnc
* imsi
* smsp
* ki
* opc
A commandline that makes use of the CSV write feature would look like this:
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_input_csv_file> --read-iccid --source csv --write-csv <path_to_output_csv_file>``
Batch programming
~~~~~~~~~~~~~~~~~
In case larger card batches need to be programmed, it is possible to use the ``--batch`` parameter to run ``pySim-prog`` in batch mode.
The batch mode will prompt the user to insert a card. Once a card is detected in the reader, the programming is carried out. The user may then remove the card again and the process starts over. This allows for a quick and efficient card programming without permanent commandline interaction.
You also need to enter some parameters to select the device :
-t TYPE : type of card (supersim, magicsim, fakemagicsim or try 'auto')
-d DEV : Serial port device (default /dev/ttyUSB0)
-b BAUD : Baudrate (default 9600)
pySim-read
----------
``pySim-read`` allows to read some of the most important data items from a SIM
card. This means it will only read some files of the card, and will only read
files accessible to a normal user (without any special authentication)
``pySim-read`` allows you to read some data from a SIM card. It will only some files
of the card, and will only read files accessible to a normal user (without any special authentication)
These days, it is recommended to use the ``export`` command of ``pySim-shell``
instead. It performs a much more comprehensive export of all of the [standard]
files that can be found on the card. To get a human-readable decode instead of
the raw hex export, you can use ``export --json``.
These days, you should use the ``export`` command of ``pySim-shell``
instead. It performs a much more comprehensive export of all of the
[standard] files that can be found on the card. To get a human-readable
decode instead of the raw hex export, you can use ``export --json``.
Specifically, pySim-read will dump the following:
@@ -242,4 +98,3 @@ pySim-read usage
.. argparse::
:module: pySim-read
:func: option_parser
:prog: pySim-read.py

View File

@@ -74,6 +74,18 @@ at 9600 bps. These readers are sometimes called `Phoenix`.
:members:
pySim construct utilities
-------------------------
.. automodule:: pySim.construct
:members:
pySim TLV utilities
-------------------
.. automodule:: pySim.tlv
:members:
pySim utility functions
-----------------------

View File

@@ -1,123 +0,0 @@
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
* [by default] uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your
osmo-smdpp would be running at the host name `testsmdpplus1.example.com`. You can of course replace those
certificates with your own, whether SGP.26 derived or part of a *private root CA* setup with mathcing eUICCs.
* 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. This is actually very useful for R&D and testing, as it
doesn't require you to generate new profiles all the time. This logic of course is unsuitable for
production usage.
* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical (the ones that are stored in
the respective UPP `.der` files)
* **is absolutely insecure**, as it
* does not perform all of the mandatory certificate verification (it checks the certificate chain, but not
the expiration dates nor any CRL)
* does not evaluate/consider any *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.
supplementary files
~~~~~~~~~~~~~~~~~~~
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. You can of course replace them with custom certificates
if you're operating eSIM with a *private root CA*.
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.
commandline options
~~~~~~~~~~~~~~~~~~~
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.
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.

View File

@@ -1,46 +0,0 @@
Remote access to an UICC/eUICC
==============================
To access a card with pySim-shell, it is not strictly necessary to have physical
access to it. There are solutions that allow remote access to UICC/eUICC cards.
In this section we will give a brief overview.
osmo-remsim
-----------
osmo-remsim is a suite of software programs enabling physical/geographic
separation of a cellular phone (or modem) on the one hand side and the
UICC/eUICC card on the other side.
Using osmo-remsim, you can operate an entire fleet of modems/phones, as well as
banks of SIM cards and dynamically establish or remove the connections between
modems/phones and cards.
To access remote cards with pySim-shell via osmo-remseim (RSPRO), the
provided libifd_remsim_client would be used to provide a virtual PC/SC reader
on the local machine. pySim-shell can then access this reader like any other
PC/SC reader.
More information on osmo-remsim can be found under:
* https://osmocom.org/projects/osmo-remsim/wiki
* https://ftp.osmocom.org/docs/osmo-remsim/master/osmo-remsim-usermanual.pdf
Android APDU proxy
------------------
Android APDU proxy is an Android app that provides a bridge between a host
computer and the UICC/eUICC slot of an Android smartphone.
The APDU proxy connects to VPCD server that runs on the remote host (in this
case the local machine where pySim-shell is running). The VPCD server then
provides a virtual PC/SC reader, that pySim-shell can access like any other
PC/SC reader.
On the Android side the UICC/eUICC is accessed via OMAPI (Open Mobile API),
which is available in Android since API level Android 8 (API level 29).
More information Android APDU proxy can be found under:
* https://gitea.osmocom.org/sim-card/android-apdu-proxy

File diff suppressed because it is too large Load Diff

View File

@@ -1,270 +0,0 @@
Guide: Enabling 5G SUCI
=======================
SUPI/SUCI Concealment is a feature of 5G-Standalone (SA) to encrypt the
IMSI/SUPI with a network operator public key. 3GPP Specifies two different
variants for this:
* SUCI calculation *in the UE*, using key data from the SIM
* SUCI calculation *on the card itself*
pySim supports writing the 5G-specific files for *SUCI calculation in the UE* on USIM cards, assuming
that your cards contain the required files, and you have the privileges/credentials to write to them.
This is the case using sysmocom sysmoISIM-SJA2 or any flavor of sysmoISIM-SJA5.
There is no 3GPP/ETSI standard method for configuring *SUCI calculation on the card*; pySim currently
supports the vendor-specific method for the sysmoISIM-SJA5-S17).
This document describes both methods.
Technical References
~~~~~~~~~~~~~~~~~~~~
This guide covers the basic workflow of provisioning SIM cards with the 5G SUCI feature. For detailed information on the SUCI feature and file contents, the following documents are helpful:
* USIM files and structure: `3GPP TS 31.102 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131102/16.06.00_60/ts_131102v160600p.pdf>`__
* USIM tests (incl. file content examples): `3GPP TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__
* Test keys for SUCI calculation: `3GPP TS 33.501 <https://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.pdf>`__
For specific information on sysmocom SIM cards, refer to
* the `sysmoISIM-SJA5 User Manual <https://sysmocom.de/manuals/sysmoisim-sja5-manual.pdf>`__ for the curent
sysmoISIM-SJA5 product
* the `sysmoISIM-SJA2 User Manual <https://sysmocom.de/manuals/sysmousim-manual.pdf>`__ for the older
sysmoISIM-SJA2 product
--------------
Enabling 5G SUCI *calculated in the UE*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In short, you can enable *SUCI calculation in the UE* with these steps:
* activate USIM **Service 124**
* make sure USIM **Service 125** is disabled
* store the public keys in **EF.SUCI_Calc_Info**
* set the **Routing Indicator** (required)
If you want to disable the feature, you can just disable USIM Service 124 (and 125) in `EF.UST`.
Admin PIN
---------
The usual way to authenticate yourself to the card as the cellular
operator is to validate the so-called ADM1 (admin) PIN. This may differ
from card model/vendor to card model/vendor.
Start pySIM-shell and enter the admin PIN for your card. If you bought
the SIM card from your network operator and dont have the admin PIN,
you cannot change SIM contents!
Launch pySIM:
::
$ ./pySim-shell.py -p 0
Using PC/SC reader interface
Autodetected card type: sysmoISIM-SJA2
Welcome to pySim-shell!
pySIM-shell (00:MF)>
Enter the ADM PIN:
::
pySIM-shell (00:MF)> verify_adm XXXXXXXX
Otherwise, write commands will fail with ``SW Mismatch: Expected 9000 and got 6982.``
Key Provisioning
----------------
::
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 (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> read_binary_decoded
missing Protection Scheme Identifier List data object tag
9000: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -> {}
The following JSON config defines the testfile from 3GPP TS 31.121, Section 4.9.4 with
test keys from 3GPP TS 33.501, Annex C.4. Highest priority (``0``) has a
Profile-B (``identifier: 2``) key in key slot ``1``, which means the key
with ``hnet_pubkey_identifier: 27``.
.. code:: json
{
"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": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"},
{"hnet_pubkey_identifier": 30,
"hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]
}
Write the config to file (must be single-line input as for now):
::
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": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"}, {"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.
Routing Indicator
-----------------
The Routing Indicator must be present for the SUCI feature. By default,
the contents of the file is **invalid** (ffffffff):
::
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
Indicator goes into bytes 0 and 1 (the other bytes are reserved). To set
the Routing Indicator to 0x71:
::
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.
USIM Service Table
------------------
First, check out the USIM Service Table (UST):
::
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 3GPP TS 31.102
:widths: 15 40
:header-rows: 1
* - Service No.
- Description
* - 122
- 5GS Mobility Management Information
* - 123
- 5G Security Parameters
* - 124
- Subscription identifier privacy support
* - 125
- SUCI calculation by the USIM
* - 126
- UAC Access Identities support
* - 129
- 5GS Operator PLMN List
If youd like to enable/disable any UST service:
::
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 youre good to go. The
sysmoISIM-SJA2 does not support on-SIM calculation, so service 125 must
be disabled.
USIM Error with 5G and sysmoISIM
--------------------------------
sysmoISIM-SJA2 come 5GS-enabled. By default however, the configuration stored
in the card file-system is **not valid** for 5G networks: Service 124 is enabled,
but EF.SUCI_Calc_Info and EF.Routing_Indicator are empty files (hence
do not contain valid data).
At least for Qualcomms X55 modem, this results in an USIM error and the
whole modem shutting 5G down. If you dont need SUCI concealment but the
smartphone refuses to connect to any 5G network, try to disable the UST
service 124.
sysmoISIM-SJA5 are shipped with a more forgiving default, with valid EF.Routing_Indicator
contents and disabled Service 124
SUCI calculation by the USIM
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The SUCI calculation can also be performed by the USIM application on the UICC
directly. The UE then uses the GET IDENTITY command (see also 3GPP TS 31.102,
section 7.5) to retrieve a SUCI value.
The sysmoISIM-SJA5-S17 supports *SUCI calculation by the USIM*. The configuration
is not much different to the above described configuration of *SUCI calculation
in the UE*.
The main difference is how the key provisioning is done. When the SUCI
calculation is done by the USIM, then the key material is not accessed by the
UE. The specification (see also 3GPP TS 31.102, section 7.5.1.1), also does not
specify any file or file format to store the key material. This means the exact
way to perform the key provisioning is an implementation detail of the USIM
card application.
In the case of sysmoISIM-SJA5-S17, the key material for *SUCI calculation by the USIM* is stored in
`ADF.USIM/DF.SAIP/EF.SUCI_Calc_Info` (**not** in `ADF.USIM/DF.5GS/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.SAIP
pySIM-shell (00:MF/ADF.USIM/DF.SAIP)> select EF.SUCI_Calc_Info
The file format is exactly the same as specified in 3GPP TS 31.102, section
4.4.11.8. This means the above described key provisioning procedure can be
applied without any changes, except that the file location is different.
To signal to the UE that the USIM is setup up for SUCI calculation, service
125 must be enabled in addition to service 124 (see also 3GPP TS 31.102,
section 5.3.48)
::
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 124
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 125
To verify that the SUCI calculation works as expected, it is possible to issue
a GET IDENTITY command using pySim-shell:
::
select ADF.USIM
get_identity
The USIM should then return a SUCI TLV Data object that looks like this:
::
SUCI TLV Data Object: 0199f90717ff021b027a2c58ce1c6b89df088a9eb4d242596dd75746bb5f3503d2cf58a7461e4fd106e205c86f76544e9d732226a4e1

View File

@@ -1,64 +0,0 @@
pySim-trace
===========
pySim-trace is a utility for high-level decode of APDU protocol traces such as those obtained with
`Osmocom SIMtrace2 <https://osmocom.org/projects/simtrace2/wiki>`_ or `osmo-qcdiag <https://osmocom.org/projects/osmo-qcdiag/wiki>`_.
pySim-trace leverages the existing knowledge of pySim-shell on anything related to SIM cards,
including the structure/encoding of the various files on SIM/USIM/ISIM/HPSIM cards, and applies this
to decoding protocol traces. This means that it shows not only the name of the command (like READ
BINARY), but actually understands what the currently selected file is, and how to decode the
contents of that file.
pySim-trace also understands the parameters passed to commands and how to decode them, for example
of the AUTHENTICATE command within the USIM/ISIM/HPSIM application.
Demo
----
To get an idea how pySim-trace usage looks like, you can watch the relevant part of the 11/2022
SIMtrace2 tutorial whose `recording is freely accessible <https://media.ccc.de/v/osmodevcall-20221019-laforge-simtrace2-tutorial#t=2134>`_.
Running pySim-trace
-------------------
Running pySim-trace requires you to specify the *source* of the to-be-decoded APDUs. There are several
supported options, each with their own respective parameters (like a file name for PCAP decoding).
See the detailed command line reference below for details.
A typical execution of pySim-trace for doing live decodes of *GSMTAP (SIM APDU)* e.g. from SIMtrace2 or
osmo-qcdiag would look like this:
::
./pySim-trace.py gsmtap-udp
This binds to the default UDP port 4729 (GSMTAP) on localhost (127.0.0.1), and decodes any APDUs received
there.
pySim-trace command line reference
----------------------------------
.. argparse::
:module: pySim-trace
:func: option_parser
:prog: pySim-trace.py
Constraints
-----------
* In order to properly track the current location in the filesystem tree and other state, it is
important that the trace you're decoding includes all of the communication with the SIM, ideally
from the very start (power up).
* pySim-trace currently only supports ETSI UICC (USIM/ISIM/HPSIM) and doesn't yet support legacy GSM
SIM. This is not a fundamental technical constraint, it's just simply that nobody got around
developing and testing that part. Contributions are most welcome.

View File

@@ -1,57 +0,0 @@
WebSocket Remote Card (WSRC)
============================
WSRC (*Web Socket Remote Card*) is a mechanism by which card readers can be made remotely available
via a computer network. The transport mechanism is (as the name implies) a WebSocket. This transport
method was chosen to be as firewall/NAT friendly as possible.
WSRC Network Architecture
-------------------------
In a WSRC network, there are three major elements:
* The **WSRC Card Client** which exposes a locally attached smart card (usually via a Smart Card Reader)
to a remote *WSRC Server*
* The **WSRC Server** manges incoming connections from both *WSRC Card Clients* as well as *WSRC User Clients*
* The **WSRC User Client** is a user application, like for example pySim-shell, which is accessing a remote
card by connecting to the *WSRC Server* which relays the information to the selected *WSRC Card Client*
WSRC Protocol
-------------
The WSRC protocl consits of JSON objects being sent over a websocket. The websocket communication itself
is based on HTTP and should usually operate via TLS for security reasons.
The detailed protocol is currently still WIP. The plan is to document it here.
pySim implementations
---------------------
wsrc_server
~~~~~~~~~~~~~~~~
.. argparse::
:filename: ../contrib/wsrc_server.py
:func: parser
:prog: contrib/wsrc_server.py
wsrc_card_client
~~~~~~~~~~~~~~~~
.. argparse::
:filename: ../contrib/wsrc_card_client.py
:func: parser
:prog: contrib/wsrc_card_client.py
pySim-shell
~~~~~~~~~~~
pySim-shell can talk to a remote card via WSRC if you use the *wsrc transport*, for example like this:
::
./pySim-shell.py --wsrc-eid 89882119900000000000000000007280 --wsrc-serer-url ws://localhost:4220/
You can specify `--wsrc-eid` or `--wsrc-iccid` to identify the remote eUICC or UICC you would like to select.

View File

@@ -1,595 +0,0 @@
#!/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 osmocom.utils import h2b, b2h, swap_nibbles
import pySim.esim.rsp as rsp
from pySim.esim import saip, PMO
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)
if output == None:
return ''
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'])
else:
# there's currently no other option in the ctxParams1 choice, so this cannot happen
raise ApiError('1.3.1', '2.2', 'ctxParams1 missing mandatory ctxParamsForCommonAuthentication')
# 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)
# enable notifications for all operations
for event in ['enable', 'disable', 'delete']:
ss.profileMetadata.add_notification(event, self.server_hostname)
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"""
# SGP.22 Section 6.3: "A normal notification function execution status (MEP Notification)
# SHALL be indicated by the HTTP status code '204' (No Content) with an empty HTTP response body"
request.setResponseCode(204)
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):
raise Exception('ECDSA signature verification failed on notification')
print("Profile Installation Final Result: ", pird['finalResult'])
# remove session state
del self.rss[transactionId]
elif pendingNotification[0] == 'otherSignedNotification':
otherSignedNotif = pendingNotification[1]
# TODO: use some kind of partially-parsed original data, don't re-encode?
euiccCertificate_bin = rsp.asn1.encode('Certificate', otherSignedNotif['euiccCertificate'])
eumCertificate_bin = rsp.asn1.encode('Certificate', otherSignedNotif['eumCertificate'])
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
ci_cert_id = cert_get_auth_key_id(eum_cert)
# Verify the validity of the eUICC certificate chain
cs = CertificateSet(self.ci_get_cert_for_pkid(ci_cert_id))
cs.add_intermediate_cert(eum_cert)
# TODO v3: otherCertsInChain
cs.verify_cert_chain(euicc_cert)
tbs_bin = rsp.asn1.encode('NotificationMetadata', otherSignedNotif['tbsOtherNotification'])
if not self._ecdsa_verify(euicc_cert, otherSignedNotif['euiccNotificationSignature'], tbs_bin):
raise Exception('ECDSA signature verification failed on notification')
other_notif = otherSignedNotif['tbsOtherNotification']
pmo = PMO.from_bitstring(other_notif['profileManagementOperation'])
eid = euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
iccid = other_notif.get('iccid', None)
if iccid:
iccid = swap_nibbles(b2h(iccid))
print("handleNotification: EID %s: %s of %s" % (eid, pmo, iccid))
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=False)
#hs.app.run(endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem")
hs.app.run("localhost", 8000)
if __name__ == "__main__":
main(sys.argv)

57
ota_test.py Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/python3
from pySim.ota import *
from pySim.sms import SMS_SUBMIT, SMS_DELIVER, AddressField
from pySim.utils import h2b, h2b
# KIC1 + KID1 of 8988211000000515398
#KIC1 = h2b('C039ED58F7B81446105E79EBFD373038')
#KID1 = h2b('1799B93FE53F430BD7FD4810C77E1FDF')
#KIC3 = h2b('167F2576D64C8D41862954875C8D7979')
#KID3 = h2b('ECAE122B0E6AE4186D6487D50FDC0922')
# KIC1 + KID1 of 8988211000000467285
KIC1 = h2b('D0FDA31990D8D64178601317191669B4')
KID1 = h2b('D24EB461799C5E035C77451FD9404463')
KIC3 = h2b('C21DD66ACAC13CB3BC8B331B24AFB57B')
KID3 = h2b('12110C78E678C25408233076AA033615')
od = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=3, kic=KIC3,
algo_auth='triple_des_cbc2', kid_idx=3, kid=KID3)
print(od.crypt)
print(od.auth)
dialect = OtaDialectSms()
# RAM: B00000
# SIM RFM: B00010
# USIM RFM: B00011
tar = h2b('B00011')
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'}
outp = dialect.encode_cmd(od, tar, spi, apdu=b'\x00\xa4\x00\x04\x02\x3f\x00')
print("result: %s" % b2h(outp))
with_udh = b'\x02\x70\x00' + outp
print("with_udh: %s" % b2h(with_udh))
da = AddressField('12345678', 'unknown', 'isdn_e164')
#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)
print("tpdu: %s" % b2h(tpdu.toBytes()))
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'}
dialect.decode_resp(od, spi, '027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c')
spi = {'counter':'no_counter', 'ciphering':True, 'rc_cc_ds': 'cc', 'por_in_submit':False,
'por_shall_be_ciphered':False, 'por_rc_cc_ds': 'cc', 'por': 'por_required'}
dialect.decode_resp(od, spi, '027100001612b000110000000000000055f47118381175fb01612f')
spi = {'counter':'no_counter', 'ciphering':True, 'rc_cc_ds': 'cc', 'por_in_submit':False,
'por_shall_be_ciphered':False, 'por_rc_cc_ds': 'no_rc_cc_ds', 'por': 'por_required'}
dialect.decode_resp(od, spi, '027100000e0ab000110000000000000001612f')

View File

@@ -25,168 +25,185 @@
#
import hashlib
import argparse
from optparse import OptionParser
import os
import random
import re
import sys
import traceback
import json
import csv
from osmocom.utils import h2b, swap_nibbles, rpad
from pySim.commands import SimCardCommands
from pySim.transport import init_reader, argparse_add_reader_args
from pySim.legacy.cards import _cards_classes, card_detect
from pySim.utils import derive_milenage_opc, calculate_luhn, dec_iccid
from pySim.profile.ts_51_011 import EF_AD
from pySim.legacy.ts_51_011 import EF
from pySim.transport import init_reader
from pySim.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.card_handler import *
from pySim.utils import *
def parse_options():
parser = argparse.ArgumentParser()
argparse_add_reader_args(parser)
parser = OptionParser(usage="usage: %prog [options]")
parser.add_argument("-t", "--type", dest="type",
help="Card type (user -t list to view) [default: %(default)s]",
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]",
default="auto",
)
parser.add_argument("-T", "--probe", dest="probe",
parser.add_option("-T", "--probe", dest="probe",
help="Determine card type",
default=False, action="store_true"
)
parser.add_argument("-a", "--pin-adm", dest="pin_adm",
parser.add_option("-a", "--pin-adm", dest="pin_adm",
help="ADM PIN used for provisioning (overwrites default)",
)
parser.add_argument("-A", "--pin-adm-hex", dest="pin_adm_hex",
parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
help="ADM PIN used for provisioning, as hex string (16 characters long",
)
parser.add_argument("-e", "--erase", dest="erase", action='store_true',
help="Erase beforehand [default: %(default)s]",
parser.add_option("-e", "--erase", dest="erase", action='store_true',
help="Erase beforehand [default: %default]",
default=False,
)
parser.add_argument("-S", "--source", dest="source",
help="Data Source[default: %(default)s]",
parser.add_option("-S", "--source", dest="source",
help="Data Source[default: %default]",
default="cmdline",
)
# if mode is "cmdline"
parser.add_argument("-n", "--name", dest="name",
help="Operator name [default: %(default)s]",
parser.add_option("-n", "--name", dest="name",
help="Operator name [default: %default]",
default="Magic",
)
parser.add_argument("-c", "--country", dest="country", type=int, metavar="CC",
help="Country code [default: %(default)s]",
parser.add_option("-c", "--country", dest="country", type="int", metavar="CC",
help="Country code [default: %default]",
default=1,
)
parser.add_argument("-x", "--mcc", dest="mcc",
help="Mobile Country Code [default: %(default)s]",
parser.add_option("-x", "--mcc", dest="mcc", type="string",
help="Mobile Country Code [default: %default]",
default="901",
)
parser.add_argument("-y", "--mnc", dest="mnc",
help="Mobile Network Code [default: %(default)s]",
parser.add_option("-y", "--mnc", dest="mnc", type="string",
help="Mobile Network Code [default: %default]",
default="55",
)
parser.add_argument("--mnclen", dest="mnclen",
help="Length of Mobile Network Code [default: %(default)s]",
default="auto",
choices=["2", "3", "auto"],
parser.add_option("--mnclen", dest="mnclen", type="choice",
help="Length of Mobile Network Code [default: %default]",
default=2,
choices=[2, 3],
)
parser.add_argument("-m", "--smsc", dest="smsc",
parser.add_option("-m", "--smsc", dest="smsc",
help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
)
parser.add_argument("-M", "--smsp", dest="smsp",
parser.add_option("-M", "--smsp", dest="smsp",
help="Raw SMSP content in hex [default: auto from SMSC]",
)
parser.add_argument("-s", "--iccid", dest="iccid", metavar="ID",
parser.add_option("-s", "--iccid", dest="iccid", metavar="ID",
help="Integrated Circuit Card ID",
)
parser.add_argument("-i", "--imsi", dest="imsi",
parser.add_option("-i", "--imsi", dest="imsi",
help="International Mobile Subscriber Identity",
)
parser.add_argument("--msisdn", dest="msisdn",
parser.add_option("--msisdn", dest="msisdn",
help="Mobile Subscriber Integrated Services Digital Number",
)
parser.add_argument("-k", "--ki", dest="ki",
parser.add_option("-k", "--ki", dest="ki",
help="Ki (default is to randomize)",
)
parser.add_argument("-o", "--opc", dest="opc",
parser.add_option("-o", "--opc", dest="opc",
help="OPC (default is to randomize)",
)
parser.add_argument("--op", dest="op",
parser.add_option("--op", dest="op",
help="Set OP to derive OPC from OP and KI",
)
parser.add_argument("--acc", dest="acc",
parser.add_option("--acc", dest="acc",
help="Set ACC bits (Access Control Code). not all card types are supported",
)
parser.add_argument("--opmode", dest="opmode",
parser.add_option("--opmode", dest="opmode", type="choice",
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_argument("-f", "--fplmn", dest="fplmn", action="append",
help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
)
parser.add_argument("--epdgid", dest="epdgid",
parser.add_option("--epdgid", dest="epdgid",
help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
)
parser.add_argument("--epdgSelection", dest="epdgSelection",
parser.add_option("--epdgSelection", dest="epdgSelection",
help="Set PLMN for ePDG Selection Information. (Only Operator Identifier FQDN format supported)",
)
parser.add_argument("--pcscf", dest="pcscf",
parser.add_option("--pcscf", dest="pcscf",
help="Set Proxy Call Session Control Function (P-CSCF) Address. (Only FQDN format supported)",
)
parser.add_argument("--ims-hdomain", dest="ims_hdomain",
parser.add_option("--ims-hdomain", dest="ims_hdomain",
help="Set IMS Home Network Domain Name in FQDN format",
)
parser.add_argument("--impi", dest="impi",
parser.add_option("--impi", dest="impi",
help="Set IMS private user identity",
)
parser.add_argument("--impu", dest="impu",
parser.add_option("--impu", dest="impu",
help="Set IMS public user identity",
)
parser.add_argument("--read-imsi", dest="read_imsi", action="store_true",
parser.add_option("--read-imsi", dest="read_imsi", action="store_true",
help="Read the IMSI from the CARD", default=False
)
parser.add_argument("--read-iccid", dest="read_iccid", action="store_true",
parser.add_option("--read-iccid", dest="read_iccid", action="store_true",
help="Read the ICCID from the CARD", default=False
)
parser.add_argument("-z", "--secret", dest="secret", metavar="STR",
parser.add_option("-z", "--secret", dest="secret", metavar="STR",
help="Secret used for ICCID/IMSI autogen",
)
parser.add_argument("-j", "--num", dest="num", type=int,
parser.add_option("-j", "--num", dest="num", type=int,
help="Card # used for ICCID/IMSI autogen",
)
parser.add_argument("--batch", dest="batch_mode",
help="Enable batch mode [default: %(default)s]",
parser.add_option("--batch", dest="batch_mode",
help="Enable batch mode [default: %default]",
default=False, action='store_true',
)
parser.add_argument("--batch-state", dest="batch_state", metavar="FILE",
parser.add_option("--batch-state", dest="batch_state", metavar="FILE",
help="Optional batch state file",
)
# if mode is "csv"
parser.add_argument("--read-csv", dest="read_csv", metavar="FILE",
parser.add_option("--read-csv", dest="read_csv", metavar="FILE",
help="Read parameters from CSV file rather than command line")
parser.add_argument("--write-csv", dest="write_csv", metavar="FILE",
parser.add_option("--write-csv", dest="write_csv", metavar="FILE",
help="Append generated parameters in CSV file",
)
parser.add_argument("--write-hlr", dest="write_hlr", metavar="FILE",
parser.add_option("--write-hlr", dest="write_hlr", metavar="FILE",
help="Append generated parameters to OpenBSC HLR sqlite3",
)
parser.add_argument("--dry-run", dest="dry_run",
parser.add_option("--dry-run", dest="dry_run",
help="Perform a 'dry run', don't actually program the card",
default=False, action="store_true")
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
parser.add_option("--card_handler", dest="card_handler_config", metavar="FILE",
help="Use automatic card handling machine")
options = parser.parse_args()
(options, args) = parser.parse_args()
if options.type == 'list':
for kls in _cards_classes:
@@ -197,13 +214,15 @@ def parse_options():
return options
if options.source == 'csv':
if (options.imsi is None) and (options.iccid is None) and (options.read_imsi is False) and (options.read_iccid is False):
parser.error("CSV mode requires one additional parameter: --read-iccid, --read-imsi, --iccid or --imsi")
if (options.imsi is None) and (options.batch_mode is False) and (options.read_imsi is False) and (options.read_iccid is False):
parser.error(
"CSV mode needs either an IMSI, --read-imsi, --read-iccid or batch mode")
if options.read_csv is None:
parser.error("CSV mode requires a CSV input file")
elif options.source == 'cmdline':
if ((options.imsi is None) or (options.iccid is None)) and (options.num is None):
parser.error("If either IMSI or ICCID isn't specified, num is required")
parser.error(
"If either IMSI or ICCID isn't specified, num is required")
else:
parser.error("Only `cmdline' and `csv' sources supported")
@@ -218,6 +237,9 @@ 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
@@ -296,15 +318,8 @@ def gen_parameters(opts):
# MCC always has 3 digits
mcc = lpad(mcc, 3, "0")
# The MNC must be at least 2 digits long. This is also the most common case.
# The user may specify an explicit length using the --mnclen option.
if opts.mnclen != "auto":
if len(mnc) > int(opts.mnclen):
raise ValueError('mcc is longer than specified in option --mnclen')
mnc = lpad(mnc, int(opts.mnclen), "0")
else:
mnc = lpad(mnc, 2, "0")
# MNC must be at least 2 digits
mnc = lpad(mnc, 2, "0")
# Digitize country code (2 or 3 digits)
cc_digits = _cc_digits(opts.country)
@@ -331,8 +346,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, 18) and not _isnum(iccid, 19) and not _isnum(iccid, 20):
raise ValueError('ICCID must be 18, 19 or 20 digits !')
if not _isnum(iccid, 19) and not _isnum(iccid, 20):
raise ValueError('ICCID must be 19 or 20 digits !')
else:
if opts.num is None:
@@ -475,7 +490,6 @@ def gen_parameters(opts):
'impi': opts.impi,
'impu': opts.impu,
'opmode': opts.opmode,
'fplmn': opts.fplmn,
}
@@ -510,86 +524,40 @@ def write_params_csv(opts, params):
f.close()
def find_row_in_csv_file(csv_file_name:str, num=None, iccid=None, imsi=None):
"""
Pick a matching row in a CSV file by row number or ICCID or IMSI. When num
is not None, the search parameters iccid and imsi are ignored. When
searching for a specific ICCID or IMSI the caller must set num to None. It
is possible to search for an ICCID or an IMSI at the same time. The first
line that either contains a matching ICCID or IMSI is returned. Unused
search parameters must be set to None.
"""
f = open(csv_file_name, 'r')
def _read_params_csv(opts, iccid=None, imsi=None):
import csv
f = open(opts.read_csv, 'r')
cr = csv.DictReader(f)
# Make sure the CSV file contains at least the fields we are searching for
if not 'iccid' in cr.fieldnames:
raise Exception("wrong CSV file format - no field \"iccid\" or missing header!")
if not 'imsi' in cr.fieldnames:
raise Exception("wrong CSV file format - no field \"imsi\" or missing header!")
# Enforce at least one search parameter
if not num and not iccid and not imsi:
raise Exception("no CSV file search parameters!")
# Lower-case fieldnames
cr.fieldnames = [field.lower() for field in cr.fieldnames]
i = 0
if not 'iccid' in cr.fieldnames:
raise Exception("CSV file in wrong format!")
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 num == i:
if opts.num is not None and opts.read_iccid is False and opts.read_imsi is False:
if opts.num == i:
f.close()
return row
# Pick the first row that contains the specified ICCID
i += 1
if row['iccid'] == iccid:
f.close()
return row
# Pick the first row that contains the specified IMSI
if row['imsi'] == imsi:
f.close()
return row
i += 1
f.close()
print("Could not read card parameters from CSV file, no matching entry found.")
return None
def read_params_csv(opts, imsi=None, iccid=None):
"""
Read the card parameters from a CSV file. This function will generate the
same dictionary that gen_parameters would generate from parameters passed as
commandline arguments.
"""
row = find_row_in_csv_file(opts.read_csv, opts.num, iccid=iccid, imsi=imsi)
row = _read_params_csv(opts, iccid=iccid, imsi=imsi)
if row is not None:
row['mcc'] = row.get('mcc', mcc_from_imsi(row.get('imsi')))
# We cannot determine the MNC length (2 or 3 digits?) from the IMSI
# alone. In cases where the user has specified an mnclen via the
# commandline options we can use that info, otherwise we guess that
# the length is 2, which is also the most common case.
if opts.mnclen != "auto":
if opts.mnclen == "2":
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
elif opts.mnclen == "3":
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), True))
else:
raise ValueError("invalid parameter --mnclen, must be 2 or 3 or auto")
else:
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
# NOTE: We might concider to specify a new CSV field "mnclen" in our
# CSV files for a better automatization. However, this only makes sense
# when the tools and databases we export our files from will also add
# such a field.
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi')))
pin_adm = None
# We need to escape the pin_adm we get from the csv
@@ -622,9 +590,6 @@ def read_params_csv(opts, imsi=None, iccid=None):
def write_params_hlr(opts, params):
# SQLite3 OpenBSC HLR
# FIXME: The format of the osmo-hlr database has evolved, so that the code below will no longer work.
print("Warning: the database format of recent OsmoHLR versions is not compatible with pySim-prog!")
if opts.write_hlr:
import sqlite3
conn = sqlite3.connect(opts.write_hlr)
@@ -656,7 +621,7 @@ def write_params_hlr(opts, params):
conn.close()
def write_parameters_to_csv_and_hlr(opts, params):
def write_parameters(opts, params):
write_params_csv(opts, params)
write_params_hlr(opts, params)
@@ -703,46 +668,52 @@ def save_batch(opts):
fh.close()
def process_card(scc, opts, first, ch):
def process_card(opts, first, ch):
# Connect transport
ch.get(first)
# Get card
card = card_detect(opts.type, scc)
if card is None:
print("No card detected!")
return -1
# Probe only
if opts.probe:
return 0
# Erase if requested (not in dry run mode!)
if opts.dry_run is False:
# Connect transport
ch.get(first)
if opts.dry_run is False:
# Get card
card = card_detect(opts.type, scc)
if card is None:
print("No card detected!")
return -1
# Probe only
if opts.probe:
return 0
# Erase if requested
if opts.erase:
print("Formatting ...")
card.erase()
card.reset()
cp = None
# Generate parameters
if opts.source == 'cmdline':
cp = gen_parameters(opts)
elif opts.source == 'csv':
imsi = None
iccid = None
if opts.read_iccid:
if opts.dry_run:
# Connect transport
ch.get(False)
(res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
iccid = dec_iccid(res)
else:
iccid = opts.iccid
if opts.read_imsi:
elif opts.read_imsi:
if opts.dry_run:
# Connect transport
ch.get(False)
(res, _) = scc.read_binary(EF['IMSI'])
imsi = swap_nibbles(res)[3:]
else:
imsi = opts.imsi
cp = read_params_csv(opts, imsi=imsi, iccid=iccid)
if cp is None:
print("Error reading parameters from CSV file!\n")
return 2
print_parameters(cp)
@@ -753,8 +724,8 @@ def process_card(scc, opts, first, ch):
else:
print("Dry Run: NOT PROGRAMMING!")
# Write parameters to a specified CSV file or an HLR database (not the card)
write_parameters_to_csv_and_hlr(opts, cp)
# Write parameters permanently
write_parameters(opts, cp)
# Batch mode state update and save
if opts.num is not None:
@@ -772,6 +743,8 @@ if __name__ == '__main__':
# Init card reader driver
sl = init_reader(opts)
if sl is None:
exit(1)
# Create command layer
scc = SimCardCommands(transport=sl)
@@ -797,7 +770,7 @@ if __name__ == '__main__':
while 1:
try:
rc = process_card(scc, opts, first, ch)
rc = process_card(opts, first, ch)
except (KeyboardInterrupt):
print("")
print("Terminated by user!")

View File

@@ -28,24 +28,20 @@ import os
import random
import re
import sys
from osmocom.utils import h2b, h2s, swap_nibbles, rpad
from pySim.profile.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.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.commands import SimCardCommands
from pySim.transport import init_reader, argparse_add_reader_args
from pySim.exceptions import SwMatchError
from pySim.legacy.cards import card_detect, SimCard, UsimCard, IsimCard
from pySim.utils import dec_imsi, dec_iccid
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
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
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
option_parser = argparse.ArgumentParser(prog='pySim-read',
description='Legacy tool for reading some parts of a SIM card',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
argparse_add_reader_args(option_parser)
@@ -76,6 +72,8 @@ if __name__ == '__main__':
# Init card reader driver
sl = init_reader(opts)
if sl is None:
exit(1)
# Create command layer
scc = SimCardCommands(transport=sl)
@@ -88,7 +86,7 @@ if __name__ == '__main__':
scc.sel_ctrl = "0004"
# Testing for Classic SIM or UICC
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00" + "00")
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00")
if sw == '6e00':
# Just a Classic SIM
scc.cla_byte = "a0"
@@ -255,14 +253,6 @@ if __name__ == '__main__':
else:
print("EHPLMN: Can't read, response code = %s" % (sw,))
# EF.FPLMN
if usim_card.file_exists(EF_USIM_ADF_map['FPLMN']):
res, sw = usim_card.read_fplmn()
if sw == '9000':
print(f'FPLMN:\n{res}')
else:
print(f'FPLMN: Can\'t read, response code = {sw}')
# EF.UST
try:
if usim_card.file_exists(EF_USIM_ADF_map['UST']):

File diff suppressed because it is too large Load Diff

View File

@@ -6,23 +6,18 @@ import argparse
from pprint import pprint as pp
from pySim.apdu import *
from pySim.runtime import RuntimeState
from pySim.filesystem import RuntimeState
from osmocom.utils import JsonEncoder
from pySim.cards import UiccCardBase
from pySim.cards import UsimCard
from pySim.commands import SimCardCommands
from pySim.profile import CardProfile
from pySim.profile.ts_102_221 import CardProfileUICC
from pySim.ts_102_221 import CardProfileUICCSIM
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
@@ -32,30 +27,28 @@ logger = colorlog.getLogger()
# merge all of the command sets into one global set. This will override instructions,
# the one from the 'last' set in the addition below will prevail.
from pySim.apdu.ts_51_011 import ApduCommands as SimApduCommands
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
ApduCommands = UiccApduCommands + UsimApduCommands #+ GpApduCommands
ApduCommands = SimApduCommands + UiccApduCommands + UsimApduCommands #+ GpApduCommands
class DummySimLink(LinkBase):
"""A dummy implementation of the LinkBase abstract base class. Currently required
as the UiccCardBase doesn't work without SimCardCommands, which in turn require
as the UsimCard 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 UiccCardBase / SimCardCommands should be refactored to make this obsolete later."""
The UsimCard / 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(self, pdu):
def _send_apdu_raw(self, pdu):
#print("DummySimLink-apdu: %s" % pdu)
return [], '9000'
@@ -65,7 +58,7 @@ class DummySimLink(LinkBase):
def disconnect(self):
pass
def _reset_card(self):
def reset_card(self):
return 1
def get_atr(self):
@@ -77,52 +70,34 @@ class DummySimLink(LinkBase):
class Tracer:
def __init__(self, **kwargs):
# we assume a generic UICC profile; as all APDUs return 9000 in DummySimLink above,
# all CardProfileAddon (including SIM) will probe successful.
profile = CardProfileUICC()
# we assume a generic SIM + UICC + USIM + ISIM card
profile = CardProfileUICCSIM()
profile.add_application(CardApplicationUSIM())
profile.add_application(CardApplicationISIM())
profile.add_application(CardApplicationISDR())
profile.add_application(CardApplicationECASD())
scc = SimCardCommands(transport=DummySimLink())
card = UiccCardBase(scc)
card = UsimCard(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, apdu: Apdu, inst: ApduCommand):
def format_capdu(self, 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, json.dumps(inst.processed, cls=JsonEncoder)))
print("===============================")
def format_reset(self, apdu: CardReset):
"""Output a single decoded CardReset."""
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 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)
try:
apdu = self.source.read()
apdu_counter = apdu_counter + 1
except StopIteration:
print("%i APDUs parsed, stop iteration." % apdu_counter)
return 0
apdu = self.source.read()
#print(apdu)
if isinstance(apdu, CardReset):
self.rs.reset()
self.format_reset(apdu)
continue
# ask ApduDecoder to look-up (INS,CLA) + instantiate an ApduCommand derived
@@ -136,80 +111,51 @@ 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',
option_parser = argparse.ArgumentParser(prog='pySim-trace', description='Osmocom pySim high-level SIM card trace decoder',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
global_group = option_parser.add_argument_group('General Options')
global_group.add_argument('--no-suppress-select', action='store_false', dest='suppress_select',
help="""
Don't suppress displaying SELECT APDUs. We normally suppress them as they just clutter up
the output without giving any useful information. Any subsequent READ/UPDATE/... operations
on the selected file will log the file name most recently SELECTed.""")
help="Don't suppress displaying SELECT APDUs")
global_group.add_argument('--no-suppress-status', action='store_false', dest='suppress_status',
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.""")
help="Don't suppress displaying STATUS APDUs")
subparsers = option_parser.add_subparsers(help='APDU Source', dest='source', required=True)
parser_gsmtap = subparsers.add_parser('gsmtap-udp', help="""
Read APDUs from live capture by receiving GSMTAP-SIM packets on specified UDP port.
Use this for live capture from SIMtrace2 or osmo-qcdiag.""")
parser_gsmtap = subparsers.add_parser('gsmtap-udp', help='Live capture of GSMTAP-SIM on UDP port')
parser_gsmtap.add_argument('-i', '--bind-ip', default='127.0.0.1',
help='Local IP address to which to bind the UDP port')
parser_gsmtap.add_argument('-p', '--bind-port', default=4729,
help='Local UDP port')
parser_gsmtap_pyshark_pcap = subparsers.add_parser('gsmtap-pyshark-pcap', help="""
Read APDUs from PCAP file containing GSMTAP (SIM APDU) communication; processed via pyshark.
Use this if you have recorded a PCAP file containing GSMTAP (SIM APDU) e.g. via tcpdump or
wireshark/tshark.""")
parser_gsmtap_pyshark_pcap.add_argument('-f', '--pcap-file', required=True,
help='Name of the PCAP[ng] file to be read')
parser_rspro_pyshark_pcap = subparsers.add_parser('rspro-pyshark-pcap', help="""
Read APDUs from PCAP file containing RSPRO (osmo-remsim) communication; processed via pyshark.
PCAP file containing RSPRO (osmo-remsim) communication; processed via pyshark.
REQUIRES OSMOCOM PATCHED WIRESHARK!""")
parser_rspro_pyshark_pcap.add_argument('-f', '--pcap-file', required=True,
help='Name of the PCAP[ng] file to be read')
parser_rspro_pyshark_live = subparsers.add_parser('rspro-pyshark-live', help="""
Read APDUs from live capture of RSPRO (osmo-remsim) communication; processed via pyshark.
Live capture of RSPRO (osmo-remsim) communication; processed via pyshark.
REQUIRES OSMOCOM PATCHED WIRESHARK!""")
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':
s = PysharkRsproPcap(opts.pcap_file)
elif opts.source == 'rspro-pyshark-live':
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,
show_raw_apdu=opts.show_raw_apdu)
tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select)
logger.info('Entering main loop...')
tracer.main()

View File

@@ -1,3 +1,4 @@
# coding=utf-8
"""APDU (and TPDU) parser for UICC/USIM/ISIM cards.
The File (and its classes) represent the structure / hierarchy
@@ -9,7 +10,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-2024 by Harald Welte <laforge@osmocom.org>
# (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
@@ -26,15 +27,15 @@ 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 Byte, GreedyBytes
from construct import Optional as COptional
from osmocom.construct import *
from osmocom.utils import *
from pySim.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
from construct import *
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.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
"""There are multiple levels of decode:
@@ -51,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__(mcs, name, bases, namespace, **kwargs):
x = super().__new__(mcs, name, bases, namespace)
def __new__(metacls, name, bases, namespace, **kwargs):
x = super().__new__(metacls, 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))
@@ -134,12 +135,7 @@ class Apdu(Tpdu):
if callable(method):
return method()
# default case: only 9000 is success
if self.sw == b'\x90\x00':
return True
# This is not really a generic positive APDU SW but specific to UICC/SIM
if self.sw[0] == 0x91:
return True
return False
return self.sw == b'\x90\x00'
class ApduCommand(Apdu, metaclass=ApduCommandMeta):
@@ -149,10 +145,8 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
# fall-back constructs if the derived class provides no override
_construct_p1 = Byte
_construct_p2 = Byte
_construct = GreedyBytes
_construct_rsp = GreedyBytes
_tlv = None
_tlv_rsp = None
_construct = HexAdapter(GreedyBytes)
_construct_rsp = HexAdapter(GreedyBytes)
def __init__(self, cmd: BytesOrHex, rsp: Optional[BytesOrHex] = None):
"""Instantiate a new ApduCommand from give cmd + resp."""
@@ -188,39 +182,44 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
if apdu_case in [1, 2]:
# data is part of response
return cls(buffer[:5], buffer[5:])
if apdu_case in [3, 4]:
elif apdu_case in [3, 4]:
# data is part of command
lc = buffer[4]
return cls(buffer[:5+lc], buffer[5+lc:])
raise ValueError('%s: Invalid APDU Case %u' % (cls.__name__, apdu_case))
else:
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()
return []
else:
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()
return ''
else:
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')
return colored(b2h(self.sw), 'red')
else:
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
return lchan_nr_from_cla(self.cla)
else:
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())
@@ -232,7 +231,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 'p1' not in self.cmd_dict:
if not 'p1' in self.cmd_dict:
self.processed = self.to_dict()
else:
self.processed['p1'] = self.cmd_dict['p1']
@@ -271,7 +270,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
"""Does the given CLA match the CLA list of the command?."""
if not isinstance(cla, str):
cla = '%02X' % cla
cla = cla.upper()
cla = cla.lower()
# see https://github.com/PyCQA/pylint/issues/7219
# pylint: disable=no-member
for cla_match in cls._cla:
@@ -281,7 +280,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
cla_masked += 'X'
else:
cla_masked += cla[i]
if cla_masked == cla_match.upper():
if cla_masked == cla_match:
return True
return False
@@ -291,26 +290,17 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
if callable(method):
return method()
else:
return self._cmd_to_dict()
def _cmd_to_dict(self) -> Dict:
"""back-end function performing automatic decoding using _construct / _tlv."""
r = {}
method = getattr(self, '_decode_p1p2', None)
if callable(method):
r = self._decode_p1p2()
else:
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
r['p2'] = parse_construct(self._construct_p2, self.p2.to_bytes(1, 'big'))
r['p3'] = self.p3
if self.cmd_data:
if self._tlv:
ie = self._tlv()
ie.from_tlv(self.cmd_data)
r['body'] = ie.to_dict()
r = {}
method = getattr(self, '_decode_p1p2', None)
if callable(method):
r = self._decode_p1p2()
else:
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
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)
return r
return r
def rsp_to_dict(self) -> Dict:
"""Convert the Response part of the APDU to a dict."""
@@ -320,12 +310,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
else:
r = {}
if 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['body'] = parse_construct(self._construct_rsp, self.rsp_data)
r['sw'] = b2h(self.sw)
return r
@@ -438,7 +423,8 @@ class TpduFilter(ApduHandler):
apdu = Apdu(icmd, tpdu.rsp)
if self.apdu_handler:
return self.apdu_handler.input(apdu)
return Apdu(icmd, tpdu.rsp)
else:
return Apdu(icmd, tpdu.rsp)
def input(self, cmd: bytes, rsp: bytes):
if isinstance(cmd, str):
@@ -457,10 +443,4 @@ class ApduDecoder(ApduHandler):
class CardReset:
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__)
pass

View File

@@ -1,7 +1,7 @@
# coding=utf-8
"""APDU definition/decoder of GlobalPLatform Card Spec (currently 2.1.1)
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
(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
@@ -17,11 +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 FlagsEnum, Struct
from osmocom.tlv import flatten_dict_lists
from osmocom.construct import *
from pySim.apdu import ApduCommand, ApduCommandSet
from pySim.global_platform import InstallParameters
class GpDelete(ApduCommand, n='DELETE', ins=0xE4, cla=['8X', 'CX', 'EX']):
_apdu_case = 4
@@ -44,29 +40,8 @@ class GpGetDataCB(ApduCommand, n='GET DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
class GpGetStatus(ApduCommand, n='GET STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
_apdu_case = 4
# GPCS Section 11.5.2
class GpInstall(ApduCommand, n='INSTALL', ins=0xE6, cla=['8X', 'CX', 'EX']):
_apdu_case = 4
_construct_p1 = FlagsEnum(Byte, more_commands=0x80, for_registry_update=0x40,
for_personalization=0x20, for_extradition=0x10,
for_make_selectable=0x08, for_install=0x04, for_load=0x02)
_construct_p2 = Enum(Byte, no_info_provided=0x00, beginning_of_combined=0x01,
end_of_combined=0x03)
_construct = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
'module_aid'/Prefixed(Int8ub, GreedyBytes),
'application_aid'/Prefixed(Int8ub, GreedyBytes),
'privileges'/Prefixed(Int8ub, GreedyBytes),
'install_parameters'/Prefixed(Int8ub, GreedyBytes), # TODO: InstallParameters
'install_token'/Prefixed(Int8ub, GreedyBytes))
def _decode_cmd(self):
# first use _construct* above
res = self._cmd_to_dict()
# then do TLV decode of install_parameters
ip = InstallParameters()
ip.from_tlv(res['body']['install_parameters'])
res['body']['install_parameters'] = flatten_dict_lists(ip.to_dict())
return res
class GpLoad(ApduCommand, n='LOAD', ins=0xE8, cla=['8X', 'CX', 'EX']):
_apdu_case = 4

View File

@@ -1,7 +1,7 @@
# coding=utf-8
"""APDU definitions/decoders of ETSI TS 102 221, the core UICC spec.
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
(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
@@ -17,18 +17,11 @@ 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 osmocom.utils import i2h
from osmocom.construct import *
from pySim.construct import *
from pySim.filesystem import *
from pySim.runtime import RuntimeLchan
from pySim.apdu import ApduCommand, ApduCommandSet
from pySim import cat
from typing import Optional, Dict, Tuple
logger = logging.getLogger(__name__)
@@ -83,7 +76,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')
@@ -97,10 +90,10 @@ 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 = b2h(self.cmd_dict['body'])
aid = self.cmd_dict['body']
sels = lchan.rs.mf.get_app_selectables(['AIDS'])
adf = self._find_aid_substr(sels, aid)
if adf:
@@ -108,7 +101,8 @@ 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)
logger.warning('SELECT UNKNOWN AID %s' % aid)
pass
else:
raise ValueError('Select Mode %s not implemented' % mode)
# decode the SELECT response
@@ -116,7 +110,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
self.file = lchan.selected_file
if 'body' in self.rsp_dict:
# not every SELECT is asking for the FCP in response...
return lchan.selected_file.decode_select_response(b2h(self.rsp_dict['body']))
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
return None
@@ -129,7 +123,7 @@ class UiccStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
def process_on_lchan(self, lchan):
if self.cmd_dict['p2'] == 'response_like_select':
return lchan.selected_file.decode_select_response(b2h(self.rsp_dict['body']))
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
def _decode_binary_p1p2(p1, p2) -> Dict:
ret = {}
@@ -207,7 +201,7 @@ class ReadRecord(ApduCommand, n='READ RECORD', ins=0xB2, cla=['0X', '4X', '6X'])
return b2h(self.rsp_data)
method = getattr(self.file, 'decode_record_bin', None)
if self.successful and callable(method):
return method(self.rsp_data, self.cmd_dict['record_number'])
return method(self.rsp_data)
# TS 102 221 Section 11.1.6
class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['0X', '4X', '6X']):
@@ -223,7 +217,7 @@ class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['0X', '4X', '6
return b2h(self.cmd_data)
method = getattr(self.file, 'decode_record_bin', None)
if self.successful and callable(method):
return method(self.cmd_data, self.cmd_dict['record_number'])
return method(self.cmd_data)
# TS 102 221 Section 11.1.7
class SearchRecord(ApduCommand, n='SEARCH RECORD', ins=0xA2, cla=['0X', '4X', '6X']):
@@ -295,9 +289,12 @@ class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
@staticmethod
def _pin_is_success(sw):
return bool(sw[0] == 0x63)
if sw[0] == 0x63:
return True
else:
return False
def process_on_lchan(self, _lchan: RuntimeLchan):
def process_on_lchan(self, lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
@@ -309,7 +306,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):
@@ -321,7 +318,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):
@@ -332,7 +329,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):
@@ -344,7 +341,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):
@@ -396,13 +393,12 @@ class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X',
manage_channel = rs.get_lchan_by_cla(self.cla)
manage_channel.add_lchan(created_channel_nr)
self.col_id = '%02u' % created_channel_nr
return {'mode': mode, 'created_channel': created_channel_nr }
if mode == 'close_channel':
closed_channel_nr = self.cmd_dict['p2']['logical_channel_number']
elif mode == 'close_channel':
closed_channel_nr = self.cmd_dict['p2']
rs.del_lchan(closed_channel_nr)
self.col_id = '%02u' % closed_channel_nr
return {'mode': mode, 'closed_channel': closed_channel_nr }
raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
else:
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']):
@@ -420,13 +416,13 @@ class ManageSecureChannel(ApduCommand, n='MANAGE SECURE CHANNEL', ins=0x73, cla=
p2 = hdr[3]
if p1 & 0x7 == 0: # retrieve UICC Endpoints
return 2
if p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
elif 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
if p2_cmd in [1,3,5]: # response data
elif p2_cmd in [1,3,5]: # response data
return 2
if p1 & 0xf == 4: # terminate secure channel SA
elif p1 & 0xf == 4: # terminate secure channel SA
return 3
raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
@@ -437,7 +433,8 @@ class TransactData(ApduCommand, n='TRANSACT DATA', ins=0x75, cla=['0X', '4X', '6
p1 = hdr[2]
if p1 & 0x04:
return 3
return 2
else:
return 2
# TS 102 221 Section 11.1.22
class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
@@ -460,17 +457,14 @@ 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']):
@@ -493,7 +487,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):

View File

@@ -1,60 +0,0 @@
# coding=utf-8
"""APDU definitions/decoders of ETSI TS 102 222.
(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/>.
"""
import logging
from construct import Struct
from osmocom.construct import *
from pySim.apdu import ApduCommand, ApduCommandSet
from pySim.ts_102_221 import FcpTemplate
logger = logging.getLogger(__name__)
# TS 102 222 Section 6.3
class CreateFile(ApduCommand, n='CREATE FILE', ins=0xE0, cla=['0X', '4X', 'EX']):
_apdu_case = 3
_tlv = FcpTemplate
# TS 102 222 Section 6.4
class DeleteFile(ApduCommand, n='DELETE FILE', ins=0xE4, cla=['0X', '4X']):
_apdu_case = 3
_construct = Struct('file_id'/Bytes(2))
# TS 102 222 Section 6.7
class TerminateDF(ApduCommand, n='TERMINATE DF', ins=0xE6, cla=['0X', '4X']):
_apdu_case = 1
# TS 102 222 Section 6.8
class TerminateEF(ApduCommand, n='TERMINATE EF', ins=0xE8, cla=['0X', '4X']):
_apdu_case = 1
# TS 102 222 Section 6.9
class TerminateCardUsage(ApduCommand, n='TERMINATE CARD USAGE', ins=0xFE, cla=['0X', '4X']):
_apdu_case = 1
# TS 102 222 Section 6.10
class ResizeFile(ApduCommand, n='RESIZE FILE', ins=0xD4, cla=['8X', 'CX', 'EX']):
_apdu_case = 3
_construct_p1 = Enum(Byte, mode_0=0, mode_1=1)
_tlv = FcpTemplate
ApduCommands = ApduCommandSet('TS 102 222', cmds=[CreateFile, DeleteFile, TerminateDF,
TerminateEF, TerminateCardUsage, ResizeFile])

View File

@@ -9,12 +9,12 @@ APDU commands of 3GPP TS 31.102 V16.6.0
"""
from typing import Dict
from construct import BitStruct, Enum, BitsInteger, Int8ub, Bytes, this, Struct, If, Switch, Const
from construct import *
from construct import Optional as COptional
from osmocom.construct import *
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,6 +35,8 @@ 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
@@ -42,28 +44,28 @@ class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '
BitsInteger(4),
'authentication_context'/Enum(BitsInteger(3), gsm=0, umts=1,
vgcs_vbs=2, gba=4))
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, Bytes(this._autn_len)))
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/Bytes(this._vsid_len),
'_vkid_len'/Int8ub, 'vk_id'/Bytes(this._vkid_len),
'_vstk_rand_len'/Int8ub, 'vstk_rand'/Bytes(this._vstk_rand_len))
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
'_autn_len'/Int8ub, 'autn'/Bytes(this._autn_len))
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/Bytes(this._naf_id_len),
'_impi_len'/Int8ub, 'impi'/Bytes(this._impi_len))
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, HexAdapter(Bytes(this._autn_len))))
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/HexAdapter(Bytes(this._vsid_len)),
'_vkid_len'/Int8ub, 'vk_id'/HexAdapter(Bytes(this._vkid_len)),
'_vstk_rand_len'/Int8ub, 'vstk_rand'/HexAdapter(Bytes(this._vstk_rand_len)))
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
'_autn_len'/Int8ub, 'autn'/HexAdapter(Bytes(this._autn_len)))
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/HexAdapter(Bytes(this._naf_id_len)),
'_impi_len'/Int8ub, 'impi'/HexAdapter(Bytes(this._impi_len)))
_cs_cmd_gba = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDD: 'bootstrap'/_cmd_gba_bs,
0xDE: 'naf_derivation'/_cmd_gba_naf }))
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/Bytes(this._len_sres),
'_len_kc'/Int8ub, 'kc'/Bytes(this._len_kc))
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/Bytes(this._len_res),
'_len_ck'/Int8ub, 'ck'/Bytes(this._len_ck),
'_len_ik'/Int8ub, 'ik'/Bytes(this._len_ik),
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, Bytes(this._len_kc)))
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/Bytes(this._len_auts))
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/HexAdapter(Bytes(this._len_sres)),
'_len_kc'/Int8ub, 'kc'/HexAdapter(Bytes(this._len_kc)))
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/HexAdapter(Bytes(this._len_res)),
'_len_ck'/Int8ub, 'ck'/HexAdapter(Bytes(this._len_ck)),
'_len_ik'/Int8ub, 'ik'/HexAdapter(Bytes(this._len_ik)),
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, HexAdapter(Bytes(this._len_kc))))
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/HexAdapter(Bytes(this._len_auts)))
_cs_rsp_3g = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDB: 'success'/_rsp_3g_ok,
0xDC: 'sync_fail'/_rsp_3g_sync}))
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/Bytes(this._vstk_len))
_cs_rsp_gba_naf = Struct(Const(b'\xDB'), '_ks_ext_naf_len'/Int8ub, 'ks_ext_naf'/Bytes(this._ks_ext_naf_len))
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/HexAdapter(Bytes(this._vstk_len)))
_cs_rsp_gba_naf = Struct(Const(b'\xDB'), '_ks_ext_naf_len'/Int8ub, 'ks_ext_naf'/HexAdapter(Bytes(this._ks_ext_naf_len)))
def _decode_cmd(self) -> Dict:
r = {}
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
@@ -106,7 +108,7 @@ class UsimAuthenticateOdd(ApduCommand, n='AUTHENTICATE', ins=0x89, cla=['0X', '4
class UsimGetIdentity(ApduCommand, n='GET IDENTITY', ins=0x78, cla=['8X', 'CX', 'EX']):
_apdu_case = 4
_construct_p2 = BitStruct('scope'/Enum(Flag, mf=0, df_adf_specific=1),
'identity_context'/Enum(BitsInteger(7), suci=1, suci_5g_nswo=2))
'identity_context'/Enum(BitsInteger(7), suci=1))
_tlv_rsp = SUCI_TlvDataObject
ApduCommands = ApduCommandSet('TS 31.102', cmds=[UsimAuthenticateEven, UsimAuthenticateOdd,

337
pySim/apdu/ts_51_011.py Normal file
View File

@@ -0,0 +1,337 @@
# coding=utf-8
"""APDU definitions/decoders of 3GPP TS 51.011, the classic SIM spec.
(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/>.
"""
import logging
from pySim.construct import *
from pySim.filesystem import *
from pySim.apdu import ApduCommand, ApduCommandSet
from typing import Optional, Dict, Tuple
logger = logging.getLogger(__name__)
# TS 51.011 Section 9.2.1
class SimSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['A0']):
_apdu_case = 4
def process_on_lchan(self, lchan: RuntimeLchan):
path = [self.cmd_data[i:i+2] for i in range(0, len(self.cmd_data), 2)]
for file in path:
file_hex = b2h(file)
sels = lchan.selected_file.get_selectables(['FIDS'])
if file_hex in sels:
if self.successful:
#print("\tSELECT %s" % sels[file_hex])
lchan.selected_file = sels[file_hex]
else:
#print("\tSELECT %s FAILED" % sels[file_hex])
pass
continue
logger.warning('SELECT UNKNOWN FID %s (%s)' % (file_hex, '/'.join([b2h(x) for x in path])))
if len(self.cmd_data) != 2:
raise ValueError('Expecting a 2-byte FID')
# decode the SELECT response
if self.successful:
self.file = lchan.selected_file
if 'body' in self.rsp_dict:
# not every SELECT is asking for the FCP in response...
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
return None
# TS 51.011 Section 9.2.2
class SimStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['A0']):
_apdu_case = 2
def process_on_lchan(self, lchan):
if self.successful:
if 'body' in self.rsp_dict:
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
def _decode_binary_p1p2(p1, p2) -> Dict:
ret = {}
if p1 & 0x80:
ret['file'] = 'sfi'
ret['sfi'] = p1 & 0x1f
ret['offset'] = p2
else:
ret['file'] = 'currently_selected_ef'
ret['offset'] = ((p1 & 0x7f) << 8) & p2
return ret
# TS 51.011 Section 9.2.3 / 31.101
class ReadBinary(ApduCommand, n='READ BINARY', ins=0xB0, cla=['A0']):
_apdu_case = 2
def _decode_p1p2(self):
return _decode_binary_p1p2(self.p1, self.p2)
def process_on_lchan(self, lchan):
self._determine_file(lchan)
if not isinstance(self.file, TransparentEF):
return b2h(self.rsp_data)
# our decoders don't work for non-zero offsets / short reads
if self.cmd_dict['offset'] != 0 or self.lr < self.file.size[0]:
return b2h(self.rsp_data)
method = getattr(self.file, 'decode_bin', None)
if self.successful and callable(method):
return method(self.rsp_data)
# TS 51.011 Section 9.2.4 / 31.101
class UpdateBinary(ApduCommand, n='UPDATE BINARY', ins=0xD6, cla=['A0']):
_apdu_case = 3
def _decode_p1p2(self):
return _decode_binary_p1p2(self.p1, self.p2)
def process_on_lchan(self, lchan):
self._determine_file(lchan)
if not isinstance(self.file, TransparentEF):
return b2h(self.rsp_data)
# our decoders don't work for non-zero offsets / short writes
if self.cmd_dict['offset'] != 0 or self.lc < self.file.size[0]:
return b2h(self.cmd_data)
method = getattr(self.file, 'decode_bin', None)
if self.successful and callable(method):
return method(self.cmd_data)
def _decode_record_p1p2(p1, p2):
ret = {}
ret['record_number'] = p1
if p2 >> 3 == 0:
ret['file'] = 'currently_selected_ef'
else:
ret['file'] = 'sfi'
ret['sfi'] = p2 >> 3
mode = p2 & 0x7
if mode == 2:
ret['mode'] = 'next_record'
elif mode == 3:
ret['mode'] = 'previous_record'
elif mode == 8:
ret['mode'] = 'absolute_current'
return ret
# TS 51.011 Section 9.2.5
class ReadRecord(ApduCommand, n='READ RECORD', ins=0xB2, cla=['A0']):
_apdu_case = 2
def _decode_p1p2(self):
r = _decode_record_p1p2(self.p1, self.p2)
self.col_id = '%02u' % r['record_number']
return r
def process_on_lchan(self, lchan):
self._determine_file(lchan)
if not isinstance(self.file, LinFixedEF):
return b2h(self.rsp_data)
method = getattr(self.file, 'decode_record_bin', None)
if self.successful and callable(method):
return method(self.rsp_data)
# TS 51.011 Section 9.2.6
class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['A0']):
_apdu_case = 3
def _decode_p1p2(self):
r = _decode_record_p1p2(self.p1, self.p2)
self.col_id = '%02u' % r['record_number']
return r
def process_on_lchan(self, lchan):
self._determine_file(lchan)
if not isinstance(self.file, LinFixedEF):
return b2h(self.cmd_data)
method = getattr(self.file, 'decode_record_bin', None)
if self.successful and callable(method):
return method(self.cmd_data)
# TS 51.011 Section 9.2.7
class Seek(ApduCommand, n='SEEK', ins=0xA2, cla=['A0']):
_apdu_case = 4
_construct_rsp = GreedyRange(Int8ub)
def _decode_p1p2(self):
ret = {}
sfi = self.p2 >> 3
if sfi == 0:
ret['file'] = 'currently_selected_ef'
else:
ret['file'] = 'sfi'
ret['sfi'] = sfi
mode = self.p2 & 0x7
if mode in [0x4, 0x5]:
if mode == 0x4:
ret['mode'] = 'forward_search'
else:
ret['mode'] = 'backward_search'
ret['record_number'] = self.p1
self.col_id = '%02u' % ret['record_number']
elif mode == 6:
ret['mode'] = 'enhanced_search'
# TODO: further decode
elif mode == 7:
ret['mode'] = 'proprietary_search'
return ret
def _decode_cmd(self):
ret = self._decode_p1p2()
if self.cmd_data:
if ret['mode'] == 'enhanced_search':
ret['search_indication'] = b2h(self.cmd_data[:2])
ret['search_string'] = b2h(self.cmd_data[2:])
else:
ret['search_string'] = b2h(self.cmd_data)
return ret
def process_on_lchan(self, lchan):
self._determine_file(lchan)
return self.to_dict()
# TS 51.011 Section 9.2.8
class Increase(ApduCommand, n='INCREASE', ins=0x32, cla=['A0']):
_apdu_case = 4
PinConstructP2 = BitStruct('scope'/Enum(Flag, global_mf=0, specific_df_adf=1),
BitsInteger(2), 'reference_data_nr'/BitsInteger(5))
# TS 51.011 Section 9.2.9
class VerifyChv(ApduCommand, n='VERIFY CHV', ins=0x20, cla=['A0']):
_apdu_case = 3
_construct_p2 = PinConstructP2
@staticmethod
def _pin_process(apdu):
processed = {
'scope': apdu.cmd_dict['p2']['scope'],
'referenced_data_nr': apdu.cmd_dict['p2']['reference_data_nr'],
}
if apdu.lc == 0:
# this is just a question on the counters remaining
processed['mode'] = 'check_remaining_attempts'
else:
processed['pin'] = b2h(apdu.cmd_data)
if apdu.sw[0] == 0x63:
processed['remaining_attempts'] = apdu.sw[1] & 0xf
return processed
@staticmethod
def _pin_is_success(sw):
if sw[0] == 0x63:
return True
else:
return False
def process_on_lchan(self, lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
return VerifyPin._pin_is_success(self.sw)
# TS 51.011 Section 9.2.10
class ChangeChv(ApduCommand, n='CHANGE CHV', ins=0x24, cla=['A0']):
_apdu_case = 3
_construct_p2 = PinConstructP2
def process_on_lchan(self, lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
return VerifyPin._pin_is_success(self.sw)
# TS 51.011 Section 9.2.11
class DisableChv(ApduCommand, n='DISABLE CHV', ins=0x26, cla=['A0']):
_apdu_case = 3
_construct_p2 = PinConstructP2
def process_on_lchan(self, lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
return VerifyPin._pin_is_success(self.sw)
# TS 51.011 Section 9.2.12
class EnableChv(ApduCommand, n='ENABLE CHV', ins=0x28, cla=['A0']):
_apdu_case = 3
_construct_p2 = PinConstructP2
def process_on_lchan(self, lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
return VerifyPin._pin_is_success(self.sw)
# TS 51.011 Section 9.2.13
class UnblockChv(ApduCommand, n='UNBLOCK CHV', ins=0x2C, cla=['A0']):
_apdu_case = 3
_construct_p2 = PinConstructP2
def process_on_lchan(self, lchan: RuntimeLchan):
return VerifyPin._pin_process(self)
def _is_success(self):
return VerifyPin._pin_is_success(self.sw)
# TS 51.011 Section 9.2.14
class Invalidate(ApduCommand, n='INVALIDATE', ins=0x04, cla=['A0']):
_apdu_case = 1
_construct_p1 = BitStruct(BitsInteger(4),
'select_mode'/Enum(BitsInteger(4), ef_by_file_id=0,
path_from_mf=8, path_from_current_df=9))
# TS 51.011 Section 9.2.15
class Rehabilitate(ApduCommand, n='REHABILITATE', ins=0x44, cla=['A0']):
_apdu_case = 1
_construct_p1 = Invalidate._construct_p1
# TS 51.011 Section 9.2.16
class RunGsmAlgorithm(ApduCommand, n='RUN GSM ALGORITHM', ins=0x88, cla=['A0']):
_apdu_case = 4
_construct = Struct('rand'/HexAdapter(Bytes(16)))
_construct_rsp = Struct('sres'/HexAdapter(Bytes(4)), 'kc'/HexAdapter(Bytes(8)))
# TS 51.011 Section 9.2.17
class Sleep(ApduCommand, n='SLEEP', ins=0xFA, cla=['A0']):
_apdu_case = 2
# TS 51.011 Section 9.2.18
class GetResponse(ApduCommand, n='GET RESPONSE', ins=0xC0, cla=['A0']):
_apdu_case = 2
# TS 51.011 Section 9.2.19
class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['A0']):
_apdu_case = 3
# TS 51.011 Section 9.2.20
class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['A0']):
_apdu_case = 4
# TS 51.011 Section 9.2.21
class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['A0']):
_apdu_case = 2
# TS 51.011 Section 9.2.22
class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['A0']):
_apdu_case = 3
ApduCommands = ApduCommandSet('TS 51.011', cmds=[SimSelect, SimStatus, ReadBinary, UpdateBinary, ReadRecord,
UpdateRecord, Seek, Increase, VerifyChv, ChangeChv, DisableChv,
EnableChv, UnblockChv, Invalidate, Rehabilitate, RunGsmAlgorithm,
Sleep, GetResponse, TerminalProfile, Envelope, Fetch, TerminalResponse])

View File

@@ -14,6 +14,7 @@ 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."""
@@ -30,5 +31,5 @@ class ApduSource(abc.ABC):
elif isinstance(r, CardReset):
apdu = r
else:
raise ValueError('Unknown read_packet() return %s' % r)
ValueError('Unknown read_packet() return %s' % r)
return apdu

View File

@@ -16,16 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from osmocom.gsmtap import GsmtapReceiver
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
from pySim.gsmtap import GsmtapMessage, GsmtapSource
from . import ApduSource, PacketType, CardReset
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
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
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
class GsmtapApduSource(ApduSource):
"""ApduSource for handling GSMTAP-SIM messages received via UDP, such as
@@ -41,19 +38,19 @@ class GsmtapApduSource(ApduSource):
bind_port: UDP port number to which the socket should be bound (default: 4729)
"""
super().__init__()
self.gsmtap = GsmtapReceiver(bind_ip, bind_port)
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'])
if sub_type == 'atr':
elif sub_type == 'atr':
# card has been reset
return CardReset(gsmtap_msg['body'])
if sub_type in ['pps_req', 'pps_rsp']:
return CardReset()
elif sub_type in ['pps_req', 'pps_rsp']:
# simply ignore for now
pass
else:

View File

@@ -1,88 +0,0 @@
# coding=utf-8
# (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/>.
import logging
from typing import Tuple
import pyshark
from osmocom.gsmtap import GsmtapMessage
from pySim.utils import h2b
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
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 + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
logger = logging.getLogger(__name__)
class _PysharkGsmtap(ApduSource):
"""APDU Source [provider] base class for reading GSMTAP SIM APDU via tshark."""
def __init__(self, pyshark_inst):
self.pyshark = pyshark_inst
self.bank_id = None
self.bank_slot = None
self.cmd_tpdu = None
super().__init__()
def read_packet(self) -> PacketType:
p = self.pyshark.next()
return self._parse_packet(p)
def _set_or_verify_bank_slot(self, bsl: Tuple[int, int]):
"""Keep track of the bank:slot to make sure we don't mix traces of multiple cards"""
if not self.bank_id:
self.bank_id = bsl[0]
self.bank_slot = bsl[1]
else:
if self.bank_id != bsl[0] or self.bank_slot != bsl[1]:
raise ValueError('Received data for unexpected B(%u:%u)' % (bsl[0], bsl[1]))
def _parse_packet(self, p) -> PacketType:
udp_layer = p['udp']
udp_payload_hex = udp_layer.get_field('payload').replace(':','')
gsmtap = GsmtapMessage(h2b(udp_payload_hex))
gsmtap_msg = gsmtap.decode()
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'])
if sub_type == 'atr':
# card has been reset
return CardReset(gsmtap_msg['body'])
if sub_type in ['pps_req', 'pps_rsp']:
# simply ignore for now
pass
else:
raise ValueError('Unsupported GSMTAP-SIM sub-type %s' % sub_type)
class PysharkGsmtapPcap(_PysharkGsmtap):
"""APDU Source [provider] class for reading GSMTAP from a PCAP
file via pyshark, which in turn uses tshark (part of wireshark).
"""
def __init__(self, pcap_filename):
"""
Args:
pcap_filename: File name of the pcap file to be opened
"""
pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='gsm_sim', use_json=True, keep_packets=False)
super().__init__(pyshark_inst)

View File

@@ -16,11 +16,13 @@
# 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
from pySim.utils import h2b, b2h
from pySim.apdu import Tpdu
from . import ApduSource, PacketType, CardReset
@@ -87,7 +89,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
@@ -99,7 +101,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
@@ -115,8 +117,7 @@ class _PysharkRspro(ApduSource):
vccPresent, resetActive, clkActive = self.get_pstatus(slot_pstatus)
if vccPresent and clkActive and not resetActive:
logger.debug("RESET")
#TODO: extract ATR from RSPRO message and use it here
return CardReset(None)
return CardReset()
else:
print("Unhandled msg type %s: %s" % (msg_type, rspro_msg))

View File

@@ -1,48 +0,0 @@
# 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.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
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 + UiccAdmApduCommands + 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

View File

@@ -1,128 +0,0 @@
# (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, CardBase
from pySim.runtime import RuntimeState
from pySim.profile import CardProfile
from pySim.profile.cdma_ruim import CardProfileRUIM
from pySim.profile.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
# 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.profile.euicc
def init_card(sl: LinkBase, skip_card_init: bool = False) -> 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)
# The user may opt to skip all card initialization. In this case only the
# most basic card profile is selected. This mode is suitable for blank
# cards that need card O/S initialization using APDU scripts first.
if skip_card_init:
return None, CardBase(scc)
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
finally:
rs.reset()
return rs, card

View File

@@ -26,13 +26,11 @@ Support for the Secure Element Access Control, specifically the ARA-M inside an
#
from construct import GreedyBytes, GreedyString, Struct, Enum, Int8ub, Int16ub
from construct import *
from construct import Optional as COptional
from osmocom.construct import *
from osmocom.tlv import *
from osmocom.utils import Hexstr
from pySim.construct import *
from pySim.filesystem import *
import pySim.global_platform
from pySim.tlv import *
# various BER-TLV encoded Data Objects (DOs)
@@ -69,28 +67,30 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
if do[0] == 0x00:
self.decoded = {'generic_access_rule': 'never'}
return self.decoded
if do[0] == 0x01:
elif do[0] == 0x01:
self.decoded = {'generic_access_rule': 'always'}
return self.decoded
return ValueError('Invalid 1-byte generic APDU access rule')
else:
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))
self.decoded = {'apdu_filter': []}
self.decoded['apdu_filter'] = []
offset = 0
while offset < len(do):
self.decoded['apdu_filter'] += [{'header': b2h(do[offset:offset+4]),
'mask': b2h(do[offset+4:offset+8])}]
offset += 8 # Move offset to the beginning of the next apdu_filter object
return self.decoded
self.decoded['apdu_filter'] += {'header': b2h(do[offset:offset+4]),
'mask': b2h(do[offset+4:offset+8])}
self.decoded = res
return res
def _to_bytes(self):
if 'generic_access_rule' in self.decoded:
if self.decoded['generic_access_rule'] == 'never':
return b'\x00'
if self.decoded['generic_access_rule'] == 'always':
elif self.decoded['generic_access_rule'] == 'always':
return b'\x01'
return ValueError('Invalid 1-byte generic APDU access rule')
else:
return ValueError('Invalid 1-byte generic APDU access rule')
else:
if not 'apdu_filter' in self.decoded:
return ValueError('Invalid APDU AR DO')
@@ -115,7 +115,6 @@ 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)))
@@ -228,14 +227,16 @@ class BlockDO(BER_TLV_IE, tag=0xe7):
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
pass
# SEID v1.1 Table 4-2
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
ResponseRefreshTagDO, ResponseAramConfigDO]):
pass
# SEID v1.1 Table 5-1
class StoreCommandDoCollection(TLV_IE_Collection,
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
@@ -258,11 +259,8 @@ 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(scc, hdr: Hexstr, cmd_do, resp_cls, exp_sw='9000'):
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
and decoding the response data tlv."""
if cmd_do:
@@ -274,43 +272,47 @@ 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) = scc.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
return data
else:
return data
else:
return None
@staticmethod
def store_data(scc, do) -> bytes:
def store_data(tp, do) -> bytes:
"""Build the Command APDU for STORE DATA."""
return ADF_ARAM.xceive_apdu_tlv(scc, '80e29000', do, StoreResponseDoCollection)
return ADF_ARAM.xceive_apdu_tlv(tp, '80e29000', do, StoreResponseDoCollection)
@staticmethod
def get_all(scc):
return ADF_ARAM.xceive_apdu_tlv(scc, '80caff40', None, GetResponseDoCollection)
def get_all(tp):
return ADF_ARAM.xceive_apdu_tlv(tp, '80caff40', None, GetResponseDoCollection)
@staticmethod
def get_config(scc, v_major=0, v_minor=0, v_patch=1):
def get_config(tp, v_major=0, v_minor=0, v_patch=1):
cmd_do = DeviceConfigDO()
cmd_do.from_val_dict([{'device_interface_version_do': {
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
return ADF_ARAM.xceive_apdu_tlv(scc, '80cadf21', cmd_do, ResponseAramConfigDO)
cmd_do.from_dict([{'DeviceInterfaceVersionDO': {
'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 do_aram_get_all(self, _opts):
def __init(self):
super().__init__()
def do_aram_get_all(self, opts):
"""GET DATA [All] on the ARA-M Applet"""
res_do = ADF_ARAM.get_all(self._cmd.lchan.scc)
res_do = ADF_ARAM.get_all(self._cmd.card._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.lchan.scc)
res_do = ADF_ARAM.get_config(self._cmd.card._scc._tp)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
@@ -332,7 +334,7 @@ class ADF_ARAM(CardADF):
apdu_grp.add_argument(
'--apdu-always', action='store_true', help='APDU access is allowed')
apdu_grp.add_argument(
'--apdu-filter', help='APDU filter: multiple groups of 8 hex bytes (4 byte CLA/INS/P1/P2 followed by 4 byte mask)')
'--apdu-filter', help='APDU filter: 4 byte CLA/INS/P1/P2 followed by 4 byte mask (8 hex bytes)')
nfc_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
nfc_grp.add_argument('--nfc-always', action='store_true',
help='NFC event access is allowed')
@@ -346,46 +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 is not None:
ref_do_content += [{'aid_ref_do': opts.aid}]
if opts.aid:
ref_do_content += [{'AidRefDO': opts.aid}]
elif opts.aid_empty:
ref_do_content += [{'aid_ref_empty_do': None}]
ref_do_content += [{'dev_app_id_ref_do': opts.device_app_id}]
ref_do_content += [{'AidRefEmptyDO': None}]
ref_do_content += [{'DevAppIdRefDO': opts.device_app_id}]
if opts.pkg_ref:
ref_do_content += [{'pkg_ref_do': {'package_name_string': opts.pkg_ref}}]
ref_do_content += [{'PkgRefDO': opts.pkg_ref}]
# AR
ar_do_content = []
if opts.apdu_never:
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'never'}}]
ar_do_content += [{'ApduArDO': {'generic_access_rule': 'never'}}]
elif opts.apdu_always:
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
ar_do_content += [{'ApduArDO': {'generic_access_rule': 'always'}}]
elif opts.apdu_filter:
if len(opts.apdu_filter) % 16:
return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do))
offset = 0
apdu_filter = []
while offset < len(opts.apdu_filter):
apdu_filter += [{'header': opts.apdu_filter[offset:offset+8],
'mask': opts.apdu_filter[offset+8:offset+16]}]
offset += 16 # Move offset to the beginning of the next apdu_filter object
ar_do_content += [{'apdu_ar_do': {'apdu_filter': apdu_filter}}]
# TODO: multiple filters
ar_do_content += [{'ApduArDO': {'apdu_filter': [opts.apdu_filter]}}]
if opts.nfc_always:
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'always'}}]
elif opts.nfc_never:
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'never'}}]
ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'never'}}]
if opts.android_permissions:
ar_do_content += [{'perm_ar_do': {'permissions': opts.android_permissions}}]
d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
ar_do_content += [{'PermArDO': {'permissions': opts.android_permissions}}]
d = [{'RefArDO': [{'RefDO': ref_do_content}, {'ArDO': ar_do_content}]}]
csrado = CommandStoreRefArDO()
csrado.from_val_dict(d)
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, csrado)
csrado.from_dict(d)
res_do = ADF_ARAM.store_data(self._cmd.card._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.lchan.scc, deldo)
res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, deldo)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
@@ -415,78 +410,3 @@ sw_aram = {
class CardApplicationARAM(CardApplication):
def __init__(self):
super().__init__('ARA-M', adf=ADF_ARAM(), sw=sw_aram)
@staticmethod
def __export_get_from_dictlist(key, dictlist):
# Data objects are organized in lists that contain dictionaries, usually there is only one dictionary per
# list item. This function goes through that list and gets the value of the first dictionary that has the
# matching key.
if dictlist is None:
return None
obj = None
for d in dictlist:
obj = d.get(key, obj)
return obj
@staticmethod
def __export_ref_ar_do_list(ref_ar_do_list):
export_str = ""
ref_do_list = CardApplicationARAM.__export_get_from_dictlist('ref_do', ref_ar_do_list.get('ref_ar_do'))
ar_do_list = CardApplicationARAM.__export_get_from_dictlist('ar_do', ref_ar_do_list.get('ref_ar_do'))
if ref_do_list and ar_do_list:
# Get ref_do parameters
aid_ref_do = CardApplicationARAM.__export_get_from_dictlist('aid_ref_do', ref_do_list)
dev_app_id_ref_do = CardApplicationARAM.__export_get_from_dictlist('dev_app_id_ref_do', ref_do_list)
pkg_ref_do = CardApplicationARAM.__export_get_from_dictlist('pkg_ref_do', ref_do_list)
# Get ar_do parameters
apdu_ar_do = CardApplicationARAM.__export_get_from_dictlist('apdu_ar_do', ar_do_list)
nfc_ar_do = CardApplicationARAM.__export_get_from_dictlist('nfc_ar_do', ar_do_list)
perm_ar_do = CardApplicationARAM.__export_get_from_dictlist('perm_ar_do', ar_do_list)
# Write command-line
export_str += "aram_store_ref_ar_do"
if aid_ref_do:
export_str += (" --aid %s" % aid_ref_do)
else:
export_str += " --aid-empty"
if dev_app_id_ref_do:
export_str += (" --device-app-id %s" % dev_app_id_ref_do)
if apdu_ar_do and 'generic_access_rule' in apdu_ar_do:
export_str += (" --apdu-%s" % apdu_ar_do['generic_access_rule'])
elif apdu_ar_do and 'apdu_filter' in apdu_ar_do:
export_str += (" --apdu-filter ")
for apdu_filter in apdu_ar_do['apdu_filter']:
export_str += apdu_filter['header']
export_str += apdu_filter['mask']
if nfc_ar_do and 'nfc_event_access_rule' in nfc_ar_do:
export_str += (" --nfc-%s" % nfc_ar_do['nfc_event_access_rule'])
if perm_ar_do:
export_str += (" --android-permissions %s" % perm_ar_do['permissions'])
if pkg_ref_do:
export_str += (" --pkg-ref %s" % pkg_ref_do['package_name_string'])
export_str += "\n"
return export_str
@staticmethod
def export(as_json: bool, lchan):
# TODO: Add JSON output as soon as aram_store_ref_ar_do is able to process input in JSON format.
if as_json:
raise NotImplementedError("res_do encoder not yet implemented. Patches welcome.")
export_str = ""
export_str += "aram_delete_all\n"
res_do = ADF_ARAM.get_all(lchan.scc)
if not res_do:
return export_str.strip()
for res_do_dict in res_do.to_dict():
if not res_do_dict.get('response_all_ref_ar_do', False):
continue
for ref_ar_do_list in res_do_dict['response_all_ref_ar_do']:
export_str += CardApplicationARAM.__export_ref_ar_do_list(ref_ar_do_list)
return export_str.strip()

View File

@@ -24,11 +24,12 @@ 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."""
@@ -96,7 +97,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') is True
self.verbose = (self.cmds.get('verbose') == True)
def __print_outout(self, out):
print("")

View File

@@ -10,10 +10,10 @@ the need of manually entering the related card-individual data on every
operation with pySim-shell.
"""
# (C) 2021-2024 by Sysmocom s.f.m.c. GmbH
# (C) 2021 by Sysmocom s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier, Harald Welte
# 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
@@ -29,29 +29,18 @@ 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 osmocom.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_KEY_FIELD_NAMES = ['ICCID', 'EID', 'IMSI' ]
VALID_FIELD_NAMES = ['ICCID', 'ADM1',
'IMSI', 'PIN1', 'PIN2', 'PUK1', 'PUK2']
# check input parameters, but do nothing concrete yet
def _verify_get_data(self, fields: List[str] = [], key: str = 'ICCID', value: str = "") -> Dict[str, str]:
@@ -64,10 +53,14 @@ 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_KEY_FIELD_NAMES:
if (key not in self.VALID_FIELD_NAMES):
raise ValueError("Key field name '%s' is not a valid field name, valid field names are: %s" %
(key, str(self.VALID_KEY_FIELD_NAMES)))
(key, str(self.VALID_FIELD_NAMES)))
return {}
@@ -91,47 +84,19 @@ class CardKeyProvider(abc.ABC):
class CardKeyProviderCsv(CardKeyProvider):
"""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
"""Card key provider implementation that allows to query against a specified CSV file"""
csv_file = None
filename = None
def __init__(self, filename: str, transport_keys: dict):
def __init__(self, filename: str):
"""
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)
@@ -148,7 +113,7 @@ class CardKeyProviderCsv(CardKeyProvider):
if row[key] == value:
for f in fields:
if f in row:
rc.update({f: self._decrypt_field(f, row[f])})
rc.update({f: row[f]})
else:
raise RuntimeError("CSV-File '%s' lacks column '%s'" %
(self.filename, f))

File diff suppressed because it is too large Load Diff

View File

@@ -18,58 +18,43 @@ as described in 3GPP TS 31.111."""
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List
from bidict import bidict
from construct import Int8ub, Int16ub, Byte, Bytes, BitsInteger
from construct import Struct, Enum, BitStruct, this
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 construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from osmocom.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi, GsmString
from osmocom.utils import b2h
from pySim.utils import 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=0x86):
class Address(COMPR_TLV_IE, tag=0x06):
_construct = Struct('ton_npi'/Int8ub,
'call_number'/BcdAdapter(GreedyBytes))
'call_number'/BcdAdapter(Bytes(this._.total_len-1)))
# TS 102 223 Section 8.2
class AlphaIdentifier(COMPR_TLV_IE, tag=0x85):
class AlphaIdentifier(COMPR_TLV_IE, tag=0x05):
# FIXME: like EF.ADN
pass
# TS 102 223 Section 8.3
class Subaddress(COMPR_TLV_IE, tag=0x88):
class Subaddress(COMPR_TLV_IE, tag=0x08):
pass
# TS 102 223 Section 8.4 + TS 31.111 Section 8.4
class CapabilityConfigParams(COMPR_TLV_IE, tag=0x87):
class CapabilityConfigParams(COMPR_TLV_IE, tag=0x07):
pass
# TS 31.111 Section 8.5
class CBSPage(COMPR_TLV_IE, tag=0x8C):
class CBSPage(COMPR_TLV_IE, tag=0x0C):
pass
# TS 102 223 V15.3.0 Section 9.4
TypeOfCommand = Enum(Int8ub, refresh=0x01, more_time=0x02, poll_interval=0x03, polling_off=0x04,
set_up_event_list=0x05, set_up_call=0x10, send_ss=0x11, send_ussd=0x12,
send_short_message=0x13, send_dtmf=0x14, launch_browser=0x15, geo_location_req=0x16,
play_tone=0x20, display_text=0x21, get_inkey=0x22, get_input=0x23, select_item=0x24,
set_up_menu=0x25, provide_local_info=0x26, timer_management=0x27,
set_up_idle_mode_text=0x28, perform_card_apdu=0x30, power_on_card=0x31,
power_off_card=0x32, get_reader_status=0x33, run_at_command=0x34,
language_notification=0x35, open_channel=0x40, close_channel=0x41, receive_data=0x42,
send_data=0x43, get_channel_status=0x44, service_search=0x45, get_service_info=0x46,
declare_service=0x47, set_frames=0x50, get_frames_status=0x51, retrieve_mms=0x60,
submit_mms=0x61, display_mms=0x62, activate=0x70, contactless_state_changed=0x71,
command_container=0x72, encapsulated_session_control=0x73)
# TS 102 223 Section 8.6 + TS 31.111 Section 8.6
class CommandDetails(COMPR_TLV_IE, tag=0x81):
_construct = Struct('command_number'/Int8ub,
'type_of_command'/TypeOfCommand,
'type_of_command'/Int8ub,
'command_qualifier'/Int8ub)
# TS 102 223 Section 8.7
@@ -122,26 +107,26 @@ class DeviceIdentities(COMPR_TLV_IE, tag=0x82):
return bytes([src, dst])
# TS 102 223 Section 8.8
class Duration(COMPR_TLV_IE, tag=0x84):
class Duration(COMPR_TLV_IE, tag=0x04):
_construct = Struct('time_unit'/Enum(Int8ub, minutes=0, seconds=1, tenths_of_seconds=2),
'time_interval'/Int8ub)
# TS 102 223 Section 8.9
class Item(COMPR_TLV_IE, tag=0x8f):
class Item(COMPR_TLV_IE, tag=0x0f):
_construct = Struct('identifier'/Int8ub,
'text_string'/GsmStringAdapter(GreedyBytes))
# TS 102 223 Section 8.10
class ItemIdentifier(COMPR_TLV_IE, tag=0x90):
class ItemIdentifier(COMPR_TLV_IE, tag=0x10):
_construct = Struct('identifier'/Int8ub)
# TS 102 223 Section 8.11
class ResponseLength(COMPR_TLV_IE, tag=0x91):
class ResponseLength(COMPR_TLV_IE, tag=0x11):
_construct = Struct('minimum_length'/Int8ub,
'maximum_length'/Int8ub)
# TS 102 223 Section 8.12
class Result(COMPR_TLV_IE, tag=0x83):
class Result(COMPR_TLV_IE, tag=0x03):
GeneralResult = Enum(Int8ub,
# '0X' and '1X' indicate that the command has been performed
performed_successfully=0,
@@ -267,15 +252,12 @@ class SsString(COMPR_TLV_IE, tag=0x89):
# TS 102 223 Section 8.15
class TextString(COMPR_TLV_IE, tag=0x8D):
_test_de_encode = [
( '8d090470617373776f7264', {'dcs': 4, 'text_string': '70617373776f7264'} ),
]
class TextString(COMPR_TLV_IE, tag=0x0d):
_construct = Struct('dcs'/Int8ub, # TS 03.38
'text_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.16
class Tone(COMPR_TLV_IE, tag=0x8E):
class Tone(COMPR_TLV_IE, tag=0x0e):
_construct = Struct('tone'/Enum(Int8ub, dial_tone=0x01,
called_subscriber_busy=0x02,
congestion=0x03,
@@ -306,12 +288,12 @@ class Tone(COMPR_TLV_IE, tag=0x8E):
melody_8=0x47))
# TS 31 111 Section 8.17
class USSDString(COMPR_TLV_IE, tag=0x8A):
class USSDString(COMPR_TLV_IE, tag=0x0a):
_construct = Struct('dcs'/Int8ub,
'ussd_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.18
class FileList(COMPR_TLV_IE, tag=0x92):
class FileList(COMPR_TLV_IE, tag=0x12):
FileId=HexAdapter(Bytes(2))
_construct = Struct('number_of_files'/Int8ub,
'files'/GreedyRange(FileId))
@@ -338,7 +320,7 @@ class DefaultText(COMPR_TLV_IE, tag=0x97):
'text_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.24
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x98):
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x18):
_construct = GreedyRange(Int8ub)
class EventList(COMPR_TLV_IE, tag=0x99):
@@ -383,7 +365,7 @@ class LocationStatus(COMPR_TLV_IE, tag=0x9b):
_construct = Enum(Int8ub, normal_service=0, limited_service=1, no_service=2)
# TS 102 223 Section 8.31
class IconIdentifier(COMPR_TLV_IE, tag=0x9e):
class IconIdentifier(COMPR_TLV_IE, tag=0x1e):
_construct = Struct('icon_qualifier'/FlagsEnum(Int8ub, not_self_explanatory=1),
'icon_identifier'/Int8ub)
@@ -409,48 +391,25 @@ class AtCommand(COMPR_TLV_IE, tag=0xA8):
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.43
class ImmediateResponse(COMPR_TLV_IE, tag=0xAB):
class ImmediateResponse(COMPR_TLV_IE, tag=0x2b):
pass
# TS 102 223 Section 8.44
class DtmfString(COMPR_TLV_IE, tag=0xAC):
_construct = BcdAdapter(GreedyBytes)
# TS 102 223 Section 8.45
class Language(COMPR_TLV_IE, tag=0xAD):
_construct = HexAdapter(GreedyBytes)
# TS 31.111 Section 8.46
class TimingAdvance(COMPR_TLV_IE, tag=0xC6):
class TimingAdvance(COMPR_TLV_IE, tag=0x46):
_construct = Struct('me_status'/Enum(Int8ub, in_idle_state=0, not_in_idle_state=1),
'timing_advance'/Int8ub)
# TS 31.111 Section 8.47
class BrowserIdentity(COMPR_TLV_IE, tag=0xB0):
_construct = Enum(Int8ub, default=0, wml=1, html=2, xhtml=3, chtml=4)
# TS 31.111 Section 8.48
class Url(COMPR_TLV_IE, tag=0xB1):
_construct = GsmString(GreedyBytes)
# TS 31.111 Section 8.49
class Bearer(COMPR_TLV_IE, tag=0xB2):
SingleBearer = Enum(Int8ub, sms=0, csd=1, ussd=2, packet_Service=3)
_construct = GreedyRange(SingleBearer)
# TS 102 223 Section 8.50
class ProvisioningFileReference(COMPR_TLV_IE, tag=0xB3):
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.51
class BrowserTerminationCause(COMPR_TLV_IE, tag=0xB4):
_construct = Enum(Int8ub, user_termination=0, error_termination=1)
# TS 102 223 Section 8.52
class BearerDescription(COMPR_TLV_IE, tag=0xB5):
_test_de_encode = [
( 'b50103', {'bearer_parameters': '', 'bearer_type': 'default'} ),
]
# TS 31.111 Section 8.52.1
BearerParsCs = Struct('data_rate'/Int8ub,
'bearer_service'/Int8ub,
@@ -506,32 +465,25 @@ class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
class BufferSize(COMPR_TLV_IE, tag = 0xB9):
_construct = Int16ub
# TS 102 223 Section 8.56 + TS 31.111 Section 8.56
# TS 31.111 Section 8.56
class ChannelStatus(COMPR_TLV_IE, tag = 0xB8):
# complex decoding, depends on out-of-band context/knowledge :(
# for default / TCP Client mode: bit 8 of first byte indicates connected, 3 LSB indicate channel nr
_construct = HexAdapter(GreedyBytes)
pass
# TS 102 223 Section 8.58
class OtherAddress(COMPR_TLV_IE, tag = 0xBE):
_test_de_encode = [
( 'be052101020304', {'address': '01020304', 'type_of_address': 'ipv4'} ),
]
_construct = Struct('type_of_address'/Enum(Int8ub, ipv4=0x21, ipv6=0x57),
'address'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.59
class UiccTransportLevel(COMPR_TLV_IE, tag = 0xBC):
_test_de_encode = [
( 'bc03028000', {'port_number': 32768, 'protocol_type': 'tcp_uicc_client_remote'} ),
]
_construct = Struct('protocol_type'/Enum(Int8ub, udp_uicc_client_remote=1, tcp_uicc_client_remote=2,
tcp_uicc_server=3, udp_uicc_client_local=4,
tcp_uicc_client_local=5, direct_channel=6),
'port_number'/Int16ub)
# TS 102 223 Section 8.60
class Aid(COMPR_TLV_IE, tag=0xAF):
class Aid(COMPR_TLV_IE, tag=0x2f):
_construct = Struct('aid'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.61
@@ -571,13 +523,10 @@ class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
# TS 102 223 Section 8.70
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
_test_de_encode = [
( 'c704036e6161', '036e6161' ),
]
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.72
class TextAttribute(COMPR_TLV_IE, tag=0xD0):
class TextAttribute(COMPR_TLV_IE, tag=0x50):
pass
# TS 31.111 Section 8.72
@@ -613,7 +562,7 @@ class ItemTextAttributeList(COMPR_TLV_IE, tag=0xD1):
_construct = GreedyRange(Int8ub)
# TS 102 223 Section 8.80
class FrameIdentifier(COMPR_TLV_IE, tag=0xE8):
class FrameIdentifier(COMPR_TLV_IE, tag=0x68):
_construct = Struct('identifier'/Int8ub)
# TS 102 223 Section 8.82
@@ -634,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, do: bytes):
def _from_bytes(self, x):
r = []
i = 0
while i < len(do):
r.append(dec_xplmn_w_act(b2h(do[i:i+5])))
while i < len(x):
r.append(dec_xplmn_w_act(b2h(x[i:i+5])))
i += 5
return r
@@ -648,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'/PlmnAdapter(Bytes(3)),
_construct = Struct('mcc_mnc'/BcdAdapter(Bytes(3)),
'lac'/HexAdapter(Bytes(2)),
'rac'/Int8ub)
@@ -696,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'/PlmnAdapter(Bytes(3)))
_construct = GreedyRange('mcc_mnc'/HexAdapter(Bytes(3)))
# TS 102 223 Section 8.98
class EcatSequenceNumber(COMPR_TLV_IE, tag=0xA1):
@@ -720,7 +669,7 @@ class SaTemplate(COMPR_TLV_IE, tag=0xA3):
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.103
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0xBA):
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0x3A):
_construct = FlagsEnum(Byte, even_if_navigating_menus=0, even_if_data_call=1, even_if_voice_call=2)
# TS 102 223 Section 8.104
@@ -734,7 +683,7 @@ class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
_construct = GreedyRange(AccessTechTuple)
# TS 102 223 Section 8.107
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0xBB):
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0x3B):
pass
# TS 31.111 Section 8.108
@@ -768,194 +717,19 @@ 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
ChannelStatus, ChannelDataLength,
# 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,
@@ -963,24 +737,20 @@ class Refresh(ProactiveCmd, tag=0x01,
ApplicationSpecificRefreshData, PlmnWactList, PlmnList]):
pass
# TS 102 223 Section 6.6.4
class MoreTime(ProactiveCmd, tag=0x02,
nested=[CommandDetails, DeviceIdentities]):
nested=[CommandDetails]):
pass
# TS 102 223 Section 6.6.5
class PollInterval(ProactiveCmd, tag=0x03,
nested=[CommandDetails, DeviceIdentities, Duration]):
nested=[CommandDetails]):
pass
# TS 102 223 Section 6.6.14
class PollingOff(ProactiveCmd, tag=0x04,
nested=[CommandDetails, DeviceIdentities]):
nested=[CommandDetails]):
pass
# TS 102 223 Section 6.6.16
class SetUpEventList(ProactiveCmd, tag=0x05,
nested=[CommandDetails, DeviceIdentities, EventList]):
nested=[CommandDetails]):
pass
# TS 31.111 Section 6.6.12
@@ -1008,27 +778,20 @@ class SendShortMessage(ProactiveCmd, tag=0x13,
SMS_TPDU, IconIdentifier, TextAttribute, FrameIdentifier]):
pass
# TS 102 223 6.6.24
class SendDTMF(ProactiveCmd, tag=0x14,
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
DtmfString, IconIdentifier, TextAttribute, FrameIdentifier]):
nested=[CommandDetails]):
pass
# TS 102 223 6.6.26
class LaunchBrowser(ProactiveCmd, tag=0x15,
nested=[CommandDetails, DeviceIdentities, BrowserIdentity, Url, Bearer, ProvisioningFileReference,
TextString, AlphaIdentifier, IconIdentifier, TextAttribute, FrameIdentifier,
NetworkAccessName]):
nested=[CommandDetails]):
pass
class GeographicalLocationRequest(ProactiveCmd, tag=0x16,
nested=[CommandDetails]):
pass
# TS 102 223 6.6.5
class PlayTone(ProactiveCmd, tag=0x20,
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
Tone, Duration, IconIdentifier, TextAttribute, FrameIdentifier]):
nested=[CommandDetails]):
pass
# TS 101 220 Table 7.17 + 102 223 6.6.1/9.4 CMD=0x21
@@ -1215,8 +978,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,
@@ -1234,17 +997,17 @@ 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, context: dict = {}) -> List[TLV_IE]:
def from_bytes(self, binary: bytes) -> List[TLV_IE]:
# do a first parse step to get the CommandDetails
pcmd = ProactiveCommandBase()
pcmd.from_tlv(binary)
cmd_details = pcmd.find_cmd_details()
# then do a second decode stage for the specific
cmd_type = TypeOfCommand.encmapping[cmd_details.decoded['type_of_command']]
cmd_type = cmd_details.decoded['type_of_command']
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
@@ -1256,18 +1019,9 @@ class ProactiveCommand(TLV_IE_Collection,
def to_dict(self):
return self.decoded.to_dict()
def to_bytes(self, context: dict = {}):
def to_bytes(self):
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
ChannelData, ChannelDataLength,
ChannelStatus, BufferSize, BearerDescription,
]):
pass
# reasonable default for playing with OTA
# 010203040506070809101112131415161718192021222324252627282930313233

View File

@@ -5,7 +5,7 @@
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2010-2024 Harald Welte <laforge@gnumonks.org>
# Copyright (C) 2010-2021 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,161 +21,49 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
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 osmocom.construct import LV, filter_dict
from osmocom.utils import rpad, lpad, b2h, h2b, h2i, i2h, str_sanitize, Hexstr
from osmocom.tlv import bertlv_encode_len
from pySim.utils import sw_match, expand_hex, SwHexstr, ResTuple, SwMatchstr
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 pySim.exceptions import SwMatchError
from pySim.transport import LinkBase
from pySim.ts_102_221 import decode_select_response
# 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:
"""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):
def __init__(self, transport):
self._tp = transport
self.sel_ctrl = "0000"
self.lchan_nr = lchan_nr
# invokes the setter below
self.cla_byte = "a0"
self.scp = None # Secure Channel Protocol
self.sel_ctrl = "0000"
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
# Extract a single FCP item from TLV
def __parse_fcp(self, fcp):
# see also: ETSI TS 102 221, chapter 11.1.1.3.1 Response for MF,
# DF or ADF
from pytlv.TLV import TLV
tlvparser = TLV(['82', '83', '84', 'a5', '8a', '8b',
'8c', '80', 'ab', 'c6', '81', '88'])
@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
# pytlv is case sensitive!
fcp = fcp.lower()
if fcp[0:2] != '62':
raise ValueError(
'Tag of the FCP template does not match, expected 62 but got %s' % fcp[0:2])
# Unfortunately the spec is not very clear if the FCP length is
# coded as one or two byte vale, so we have to try it out by
# 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.
exp_tlv_len = int(fcp[2:4], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 4
else:
return 255
exp_tlv_len = int(fcp[2:6], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 6
def send_apdu(self, pdu: Hexstr, apply_lchan:bool = True) -> ResTuple:
"""Sends an APDU and auto fetch response data
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
apply_lchan : apply the currently selected lchan to the CLA byte before sending
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if apply_lchan:
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
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", apply_lchan:bool = True) -> 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.
apply_lchan : apply the currently selected lchan to the CLA byte before sending
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if apply_lchan:
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
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, apply_lchan:bool = True) -> 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
apply_lchan : apply the currently selected lchan to the CLA byte before sending
Returns:
Tuple of (decoded_data, sw)
"""
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
lc = i2h([len(cmd)]) if cmd_data else ''
le = '00' if resp_constr else ''
pdu = ''.join([cla, ins, p1, p2, lc, b2h(cmd), le])
(data, sw) = self.send_apdu(pdu, apply_lchan = apply_lchan)
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", apply_lchan:bool = True) -> 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,
apply_lchan = apply_lchan)
if not sw_match(sw, sw_exp):
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
return (rsp, sw)
# Skip FCP tag and length
tlv = fcp[skip:]
return tlvparser.parse(tlv)
# Tell the length of a record by the card response
# USIMs respond with an FCP template, which is different
@@ -184,8 +72,10 @@ class SimCardCommands:
# SIM: GSM 11.11, chapter 9.2.1 SELECT
def __record_len(self, r) -> int:
if self.sel_ctrl == "0004":
fcp_parsed = decode_select_response(r[-1])
return fcp_parsed['file_descriptor']['record_len']
tlv_parsed = self.__parse_fcp(r[-1])
file_descriptor = tlv_parsed['82']
# See also ETSI TS 102 221, chapter 11.1.1.4.3 File Descriptor
return int(file_descriptor[4:8], 16)
else:
return int(r[-1][28:30], 16)
@@ -193,16 +83,16 @@ class SimCardCommands:
# above.
def __len(self, r) -> int:
if self.sel_ctrl == "0004":
fcp_parsed = decode_select_response(r[-1])
return fcp_parsed['file_size']
tlv_parsed = self.__parse_fcp(r[-1])
return int(tlv_parsed['80'], 16)
else:
return int(r[-1][4:8], 16)
def get_atr(self) -> Hexstr:
def get_atr(self) -> str:
"""Return the ATR of the currently inserted card."""
return self._tp.get_atr()
def try_select_path(self, dir_list: List[Hexstr]) -> List[ResTuple]:
def try_select_path(self, dir_list):
""" Try to select a specified path
Args:
@@ -210,16 +100,17 @@ class SimCardCommands:
"""
rv = []
if not isinstance(dir_list, list):
if type(dir_list) is not list:
dir_list = [dir_list]
for i in dir_list:
data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i + "00")
data, sw = self._tp.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: Path) -> List[Hexstr]:
def select_path(self, dir_list):
"""Execute SELECT for an entire list/path of FIDs.
Args:
@@ -229,27 +120,27 @@ class SimCardCommands:
list of return values (FCP in hex encoding) for each element of the path
"""
rv = []
if not isinstance(dir_list, list):
if type(dir_list) is not 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: Hexstr) -> ResTuple:
def select_file(self, fid: str):
"""Execute SELECT a given file by FID.
Args:
fid : file identifier as hex string
"""
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid + "00")
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
def select_parent_df(self) -> ResTuple:
def select_parent_df(self):
"""Execute SELECT to switch to the parent DF """
return self.send_apdu_checksw(self.cla_byte + "a40304")
return self._tp.send_apdu_checksw(self.cla_byte + "a4030400")
def select_adf(self, aid: Hexstr) -> ResTuple:
def select_adf(self, aid: str):
"""Execute SELECT a given Applicaiton ADF.
Args:
@@ -257,9 +148,9 @@ class SimCardCommands:
"""
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid + "00")
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
def read_binary(self, ef, length: int = None, offset: int = 0):
"""Execute READD BINARY.
Args:
@@ -278,33 +169,19 @@ class SimCardCommands:
total_data = ''
chunk_offset = 0
while chunk_offset < length:
chunk_len = min(self.max_cmd_len, length-chunk_offset)
chunk_len = min(255, length-chunk_offset)
pdu = self.cla_byte + \
'b0%04x%02x' % (offset + chunk_offset, chunk_len)
try:
data, sw = self.send_apdu_checksw(pdu)
data, sw = self._tp.send_apdu_checksw(pdu)
except Exception as e:
e.add_note('failed to read (offset %d)' % offset)
raise e
raise ValueError('%s, failed to read (offset %d)' %
(str_sanitize(str(e)), offset))
total_data += data
chunk_offset += chunk_len
return total_data, 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 update_binary(self, ef: Path, data: Hexstr, offset: int = 0, verify: bool = False,
conserve: bool = False) -> ResTuple:
def update_binary(self, ef, data: str, offset: int = 0, verify: bool = False, conserve: bool = False):
"""Execute UPDATE BINARY.
Args:
@@ -321,38 +198,44 @@ class SimCardCommands:
# Save write cycles by reading+comparing before write
if conserve:
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
data_current, sw = self.read_binary(ef, data_length, offset)
if data_current == data:
return None, sw
self.select_path(ef)
total_data = ''
chunk_offset = 0
while chunk_offset < data_length:
chunk_len = min(self.max_cmd_len, data_length - chunk_offset)
chunk_len = min(255, 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.send_apdu_checksw(pdu)
chunk_data, chunk_sw = self._tp.send_apdu_checksw(pdu)
except Exception as e:
e.add_note('failed to write chunk (chunk_offset %d, chunk_len %d)' % (chunk_offset, chunk_len))
raise e
raise ValueError('%s, failed to write chunk (chunk_offset %d, chunk_len %d)' %
(str_sanitize(str(e)), chunk_offset, chunk_len))
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 read_record(self, ef: Path, rec_no: int) -> ResTuple:
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):
"""Execute READ RECORD.
Args:
@@ -362,23 +245,10 @@ 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.send_apdu_checksw(pdu)
return self._tp.send_apdu_checksw(pdu)
def __verify_record(self, ef: Path, 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 update_record(self, ef: Path, rec_no: int, data: Hexstr, force_len: bool = False,
verify: bool = False, conserve: bool = False, leftpad: bool = False) -> ResTuple:
def update_record(self, ef, rec_no: int, data: str, force_len: bool = False, verify: bool = False,
conserve: bool = False):
"""Execute UPDATE RECORD.
Args:
@@ -388,7 +258,6 @@ 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)
@@ -401,36 +270,39 @@ 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:
if leftpad:
data = lpad(data, rec_length * 2)
else:
data = rpad(data, rec_length * 2)
elif (len(data) // 2 < rec_length):
data = rpad(data, rec_length * 2)
# Save write cycles by reading+comparing before write
if conserve:
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
data_current, sw = self.read_record(ef, rec_no)
data_current = data_current[0:rec_length*2]
if data_current == data:
return None, sw
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
res = self.send_apdu_checksw(pdu)
res = self._tp.send_apdu_checksw(pdu)
if verify:
self.__verify_record(ef, rec_no, data)
self.verify_record(ef, rec_no, data)
return res
def record_size(self, ef: Path) -> int:
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):
"""Determine the record size of given file.
Args:
@@ -439,7 +311,7 @@ class SimCardCommands:
r = self.select_path(ef)
return self.__record_len(r)
def record_count(self, ef: Path) -> int:
def record_count(self, ef):
"""Determine the number of records in given file.
Args:
@@ -448,7 +320,7 @@ class SimCardCommands:
r = self.select_path(ef)
return self.__len(r) // self.__record_len(r)
def binary_size(self, ef: Path) -> int:
def binary_size(self, ef):
"""Determine the size of given transparent file.
Args:
@@ -458,14 +330,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) -> ResTuple:
def _retrieve_data(self, tag: int, first: bool = True):
if first:
pdu = '80cb008001%02x00' % (tag)
pdu = '80cb008001%02x' % (tag)
else:
pdu = '80cb0000'
return self.send_apdu_checksw(pdu)
pdu = '80cb000000'
return self._tp.send_apdu_checksw(pdu)
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
def retrieve_data(self, ef, tag: int):
"""Execute RETRIEVE DATA, see also TS 102 221 Section 11.3.1.
Args
@@ -479,23 +351,23 @@ class SimCardCommands:
# retrieve first block
data, sw = self._retrieve_data(tag, first=True)
total_data += data
while sw in ['62f1', '62f2']:
while sw == '62f1' or sw == '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: Hexstr, first: bool = True) -> ResTuple:
def _set_data(self, data: str, first: bool = True):
if first:
p1 = 0x80
else:
p1 = 0x00
if isinstance(data, (bytes, bytearray)):
if isinstance(data, bytes) or isinstance(data, bytearray):
data = b2h(data)
pdu = '80db00%02x%02x%s' % (p1, len(data)//2, data)
return self.send_apdu_checksw(pdu)
return self._tp.send_apdu_checksw(pdu)
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False):
"""Execute SET DATA.
Args
@@ -520,13 +392,13 @@ class SimCardCommands:
total_len = len(tlv_bin)
remaining = tlv_bin
while len(remaining) > 0:
fragment = remaining[:self.max_cmd_len]
fragment = remaining[:255]
rdata, sw = self._set_data(fragment, first=first)
first = False
remaining = remaining[self.max_cmd_len:]
remaining = remaining[255:]
return rdata, sw
def run_gsm(self, rand: Hexstr) -> ResTuple:
def run_gsm(self, rand: str):
"""Execute RUN GSM ALGORITHM.
Args:
@@ -535,9 +407,9 @@ class SimCardCommands:
if len(rand) != 32:
raise ValueError('Invalid rand')
self.select_path(['3f00', '7f20'])
return self.send_apdu_checksw('a088000010' + rand + '00', sw='9000')
return self._tp.send_apdu(self.cla_byte + '88000010' + rand)
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
def authenticate(self, rand: str, autn: str, context='3g'):
"""Execute AUTHENTICATE (USIM/ISIM).
Args:
@@ -546,9 +418,10 @@ class SimCardCommands:
context : 16 byte random data ('3g' or 'gsm')
"""
# 3GPP TS 31.102 Section 7.1.2.1
AuthCmd3G = Struct('rand'/LV, 'autn'/COptional(LV))
AuthCmd3G = Struct('rand'/LV, 'autn'/Optional(LV))
AuthResp3GSyncFail = Struct(Const(b'\xDC'), 'auts'/LV)
AuthResp3GSuccess = Struct(Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/COptional(LV))
AuthResp3GSuccess = Struct(
Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/Optional(LV))
AuthResp3G = Select(AuthResp3GSyncFail, AuthResp3GSuccess)
# build parameters
cmd_data = {'rand': rand, 'autn': autn}
@@ -556,9 +429,7 @@ class SimCardCommands:
p2 = '81'
elif context == 'gsm':
p2 = '80'
else:
raise ValueError("Unsupported context '%s'" % context)
(data, sw) = self.send_apdu_constr_checksw(
(data, sw) = self._tp.send_apdu_constr_checksw(
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
if 'auts' in data:
ret = {'synchronisation_failure': data}
@@ -566,47 +437,43 @@ class SimCardCommands:
ret = {'successful_3g_authentication': data}
return (ret, sw)
def status(self) -> ResTuple:
def status(self):
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
return self.send_apdu_checksw('80F20000')
return self._tp.send_apdu_checksw('80F20000ff')
def deactivate_file(self) -> ResTuple:
def deactivate_file(self):
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
return self.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
return self._tp.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
def activate_file(self, fid: Hexstr) -> ResTuple:
def activate_file(self, fid):
"""Execute ACTIVATE FILE command as per TS 102 221 Section 11.1.15.
Args:
fid : file identifier as hex string
"""
return self.send_apdu_checksw(self.cla_byte + '44000002' + fid)
return self._tp.send_apdu_checksw(self.cla_byte + '44000002' + fid)
def create_file(self, payload: Hexstr) -> ResTuple:
"""Execute CREATE FILE command as per TS 102 222 Section 6.3"""
return self.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
def create_file(self, payload: Hexstr):
"""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))
def resize_file(self, payload: Hexstr) -> ResTuple:
"""Execute RESIZE FILE command as per TS 102 222 Section 6.10"""
return self.send_apdu_checksw('80d40000%02x%s' % (len(payload)//2, payload))
def delete_file(self, fid: Hexstr) -> ResTuple:
def delete_file(self, fid):
"""Execute DELETE FILE command as per TS 102 222 Section 6.4"""
return self.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
return self._tp.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
def terminate_df(self, fid: Hexstr) -> ResTuple:
def terminate_df(self, fid):
"""Execute TERMINATE DF command as per TS 102 222 Section 6.7"""
return self.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
return self._tp.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
def terminate_ef(self, fid: Hexstr) -> ResTuple:
def terminate_ef(self, fid):
"""Execute TERMINATE EF command as per TS 102 222 Section 6.8"""
return self.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
return self._tp.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
def terminate_card_usage(self) -> ResTuple:
def terminate_card_usage(self):
"""Execute TERMINATE CARD USAGE command as per TS 102 222 Section 6.9"""
return self.send_apdu_checksw(self.cla_byte + 'fe000000')
return self._tp.send_apdu_checksw(self.cla_byte + 'fe000000')
def manage_channel(self, mode: str = 'open', lchan_nr: int =0) -> ResTuple:
def manage_channel(self, mode='open', lchan_nr=0):
"""Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17.
Args:
@@ -617,21 +484,21 @@ class SimCardCommands:
p1 = 0x80
else:
p1 = 0x00
pdu = self.cla_byte + '70%02x%02x' % (p1, lchan_nr)
return self.send_apdu_checksw(pdu)
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
return self._tp.send_apdu_checksw(pdu)
def reset_card(self) -> Hexstr:
def reset_card(self):
"""Physically reset the card"""
return self._tp.reset_card()
def _chv_process_sw(self, op_name: str, chv_no: int, pin_code: Hexstr, sw: SwHexstr):
def _chv_process_sw(self, op_name, chv_no, pin_code, sw):
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])))
if sw != '9000':
raise SwMatchError(sw, '9000', self._tp.sw_interpreter)
elif (sw != '9000'):
raise SwMatchError(sw, '9000')
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
def verify_chv(self, chv_no: int, code: str):
"""Verify a given CHV (Card Holder Verification == PIN)
Args:
@@ -639,7 +506,8 @@ class SimCardCommands:
code : chv code as hex string
"""
fc = rpad(b2h(code), 16)
data, sw = self.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
data, sw = self._tp.send_apdu(
self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('verify', chv_no, code, sw)
return (data, sw)
@@ -652,11 +520,12 @@ class SimCardCommands:
pin_code : new chv code as hex string
"""
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
data, sw = self.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
data, sw = self._tp.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: Hexstr, new_pin_code: Hexstr) -> ResTuple:
def change_chv(self, chv_no: int, pin_code: str, new_pin_code: str):
"""Change a given CHV (Card Holder Verification == PIN)
Args:
@@ -665,11 +534,12 @@ 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.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
data, sw = self._tp.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: Hexstr) -> ResTuple:
def disable_chv(self, chv_no: int, pin_code: str):
"""Disable a given CHV (Card Holder Verification == PIN)
Args:
@@ -678,11 +548,12 @@ class SimCardCommands:
new_pin_code : new chv code as hex string
"""
fc = rpad(b2h(pin_code), 16)
data, sw = self.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
data, sw = self._tp.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: Hexstr) -> ResTuple:
def enable_chv(self, chv_no: int, pin_code: str):
"""Enable a given CHV (Card Holder Verification == PIN)
Args:
@@ -690,30 +561,31 @@ class SimCardCommands:
pin_code : chv code as hex string
"""
fc = rpad(b2h(pin_code), 16)
data, sw = self.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
data, sw = self._tp.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: Hexstr) -> ResTuple:
def envelope(self, payload: str):
"""Send one ENVELOPE command to the SIM
Args:
payload : payload as hex string
"""
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload), apply_lchan = False)
return self._tp.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
def terminal_profile(self, payload: Hexstr) -> ResTuple:
def terminal_profile(self, payload: str):
"""Send TERMINAL PROFILE to card
Args:
payload : payload as hex string
"""
data_length = len(payload) // 2
data, sw = self.send_apdu_checksw(('80100000%02x' % data_length) + payload, apply_lchan = False)
data, sw = self._tp.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) -> Tuple[int, Hexstr, SwHexstr]:
def suspend_uicc(self, min_len_secs: int = 60, max_len_secs: int = 43200):
"""Send SUSPEND UICC to the card.
Args:
@@ -723,49 +595,38 @@ class SimCardCommands:
def encode_duration(secs: int) -> Hexstr:
if secs >= 10*24*60*60:
return '04%02x' % (secs // (10*24*60*60))
if secs >= 24*60*60:
elif secs >= 24*60*60:
return '03%02x' % (secs // (24*60*60))
if secs >= 60*60:
elif secs >= 60*60:
return '02%02x' % (secs // (60*60))
if secs >= 60:
elif secs >= 60:
return '01%02x' % (secs // 60)
return '00%02x' % secs
else:
return '00%02x' % secs
def decode_duration(enc: Hexstr) -> int:
time_unit = enc[:2]
length = h2i(enc[2:4])[0]
length = h2i(enc[2:4])
if time_unit == '04':
return length * 10*24*60*60
if time_unit == '03':
elif time_unit == '03':
return length * 24*60*60
if time_unit == '02':
elif time_unit == '02':
return length * 60*60
if time_unit == '01':
elif time_unit == '01':
return length * 60
if time_unit == '00':
elif time_unit == '00':
return length
raise ValueError('Time unit must be 0x00..0x04')
else:
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.send_apdu_checksw('8076000004' + min_dur_enc + max_dur_enc, apply_lchan = False)
data, sw = self._tp.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: 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.send_apdu_checksw('8076010008' + token, apply_lchan = False)
return (data, sw)
# GPC_SPE_034 11.3
def get_data(self, tag: int, cla: int = 0x00):
data, sw = self.send_apdu_checksw('%02xca%04x00' % (cla, tag))
return (data, sw)
# TS 31.102 Section 7.5.2
def get_identity(self, context: int) -> Tuple[Hexstr, SwHexstr]:
data, sw = self.send_apdu_checksw('807800%02x00' % (context))
data, sw = self._tp.send_apdu('%02xca%04x00' % (cla, tag))
return (data, sw)

265
pySim/construct.py Normal file
View File

@@ -0,0 +1,265 @@
from construct.lib.containers import Container, ListContainer
from construct.core import EnumIntegerString
import typing
from construct import *
from construct.core import evaluate, bytes2integer, integer2bytes, BitwisableString
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."""
# (C) 2021-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/>.
class HexAdapter(Adapter):
"""convert a bytes() type to a string of hex nibbles."""
def _decode(self, obj, context, path):
return b2h(obj)
def _encode(self, obj, context, path):
return h2b(obj)
class BcdAdapter(Adapter):
"""convert a bytes() type to a string of BCD nibbles."""
def _decode(self, obj, context, path):
return swap_nibbles(b2h(obj))
def _encode(self, obj, context, path):
return h2b(swap_nibbles(obj))
class InvertAdapter(Adapter):
"""inverse logic (false->true, true->false)."""
@staticmethod
def _invert_bool_in_obj(obj):
for k,v in obj.items():
# skip all private entries
if k.startswith('_'):
continue
if v == False:
obj[k] = True
elif v == True:
obj[k] = False
return obj
def _decode(self, obj, context, path):
return self._invert_bool_in_obj(obj)
def _encode(self, obj, context, path):
return self._invert_bool_in_obj(obj)
class Rpad(Adapter):
"""
Encoder appends padding bytes (b'\\xff') up to target size.
Decoder removes trailing padding bytes.
Parameters:
subcon: Subconstruct as defined by construct library
pattern: set padding pattern (default: b'\\xff')
"""
def __init__(self, subcon, pattern=b'\xff'):
super().__init__(subcon)
self.pattern = pattern
def _decode(self, obj, context, path):
return obj.rstrip(self.pattern)
def _encode(self, obj, context, path):
if len(obj) > self.sizeof():
raise SizeofError("Input ({}) exceeds target size ({})".format(
len(obj), self.sizeof()))
return obj + self.pattern * (self.sizeof() - len(obj))
class GsmStringAdapter(Adapter):
"""Convert GSM 03.38 encoded bytes to a string."""
def __init__(self, subcon, codec='gsm03.38', err='strict'):
super().__init__(subcon)
self.codec = codec
self.err = err
def _decode(self, obj, context, path):
return obj.decode(self.codec)
def _encode(self, obj, context, path):
return obj.encode(self.codec, self.err)
def filter_dict(d, exclude_prefix='_'):
"""filter the input dict to ensure no keys starting with 'exclude_prefix' remain."""
if not isinstance(d, dict):
return d
res = {}
for (key, value) in d.items():
if key.startswith(exclude_prefix):
continue
if type(value) is dict:
res[key] = filter_dict(value)
else:
res[key] = value
return res
def normalize_construct(c):
"""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):
r = {k: normalize_construct(v) for (k, v) in c.items()}
elif isinstance(c, ListContainer):
r = [normalize_construct(x) for x in c]
elif isinstance(c, list):
r = [normalize_construct(x) for x in c]
elif isinstance(c, EnumIntegerString):
r = str(c)
else:
r = c
return r
def parse_construct(c, raw_bin_data: bytes, length: typing.Optional[int] = None, exclude_prefix: str = '_'):
"""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)
# here we collect some shared / common definitions of data types
LV = Prefixed(Int8ub, HexAdapter(GreedyBytes))
# Default value for Reserved for Future Use (RFU) bits/bytes
# See TS 31.101 Sec. "3.4 Coding Conventions"
__RFU_VALUE = 0
# Field that packs Reserved for Future Use (RFU) bit
FlagRFU = Default(Flag, __RFU_VALUE)
# Field that packs Reserved for Future Use (RFU) byte
ByteRFU = Default(Byte, __RFU_VALUE)
# Field that packs all remaining Reserved for Future Use (RFU) bytes
GreedyBytesRFU = Default(GreedyBytes, b'')
def BitsRFU(n=1):
'''
Field that packs Reserved for Future Use (RFU) bit(s)
as defined in TS 31.101 Sec. "3.4 Coding Conventions"
Use this for (currently) unused/reserved bits whose contents
should be initialized automatically but should not be cleared
in the future or when restoring read data (unlike padding).
Parameters:
n (Integer): Number of bits (default: 1)
'''
return Default(BitsInteger(n), __RFU_VALUE)
def BytesRFU(n=1):
'''
Field that packs Reserved for Future Use (RFU) byte(s)
as defined in TS 31.101 Sec. "3.4 Coding Conventions"
Use this for (currently) unused/reserved bytes whose contents
should be initialized automatically but should not be cleared
in the future or when restoring read data (unlike padding).
Parameters:
n (Integer): Number of bytes (default: 1)
'''
return Default(Bytes(n), __RFU_VALUE)
def GsmString(n):
'''
GSM 03.38 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 GsmStringAdapter(Rpad(Bytes(n), pattern=b'\xff'), codec='gsm03.38')
class GreedyInteger(Construct):
"""A variable-length integer implementation, think of combining GrredyBytes with BytesInteger."""
def __init__(self, signed=False, swapped=False, minlen=0):
super().__init__()
self.signed = signed
self.swapped = swapped
self.minlen = minlen
def _parse(self, stream, context, path):
data = stream_read_entire(stream, path)
if evaluate(self.swapped, context):
data = swapbytes(data)
try:
return bytes2integer(data, self.signed)
except ValueError as e:
raise IntegerError(str(e), path=path)
def __bytes_required(self, i, minlen=0):
if self.signed:
raise NotImplementedError("FIXME: Implement support for encoding signed integer")
# compute how many bytes we need
nbytes = 1
while True:
i = i >> 8
if i == 0:
break
else:
nbytes = nbytes + 1
# round up to the minimum number
# of bytes we anticipate
if nbytes < minlen:
nbytes = minlen
return nbytes
def _build(self, obj, stream, context, path):
if not isinstance(obj, integertypes):
raise IntegerError(f"value {obj} is not an integer", path=path)
length = self.__bytes_required(obj, self.minlen)
try:
data = integer2bytes(obj, length, self.signed)
except ValueError as e:
raise IntegerError(str(e), path=path)
if evaluate(self.swapped, context):
data = swapbytes(data)
stream_write(stream, data, length, path)
return obj
# merged definitions of 24.008 + 23.040
TypeOfNumber = Enum(BitsInteger(3), unknown=0, international=1, national=2, network_specific=3,
short_code=4, alphanumeric=5, abbreviated=6, reserved_for_extension=7)
NumberingPlan = Enum(BitsInteger(4), unknown=0, isdn_e164=1, data_x121=3, telex_f69=4,
sc_specific_5=5, sc_specific_6=6, national=8, private=9,
ermes=10, reserved_cts=11, reserved_for_extension=15)
TonNpi = BitStruct('ext'/Flag, 'type_of_number'/TypeOfNumber, 'numbering_plan_id'/NumberingPlan)

View File

@@ -1,137 +0,0 @@
import sys
from typing import Optional, Tuple
from importlib import resources
class PMO:
"""Convenience conversion class for ProfileManagementOperation as used in ES9+ notifications."""
pmo4operation = {
'install': 0x80,
'enable': 0x40,
'disable': 0x20,
'delete': 0x10,
}
def __init__(self, op: str):
if not op in self.pmo4operation:
raise ValueError('Unknown operation "%s"' % op)
self.op = op
def to_int(self):
return self.pmo4operation[self.op]
@staticmethod
def _num_bits(data: int)-> int:
for i in range(0, 8):
if data & (1 << i):
return 8-i
return 0
def to_bitstring(self) -> Tuple[bytes, int]:
"""return value in a format as used by asn1tools for BITSTRING."""
val = self.to_int()
return (bytes([val]), self._num_bits(val))
@classmethod
def from_int(cls, i: int) -> 'PMO':
"""Parse an integer representation."""
for k, v in cls.pmo4operation.items():
if v == i:
return cls(k)
raise ValueError('Unknown PMO 0x%02x' % i)
@classmethod
def from_bitstring(cls, bstr: Tuple[bytes, int]) -> 'PMO':
"""Parse a asn1tools BITSTRING representation."""
return cls.from_int(bstr[0][0])
def __str__(self):
return self.op
def compile_asn1_subdir(subdir_name:str, codec='der'):
"""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=codec)
# 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)

View File

@@ -1,657 +0,0 @@
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

View File

@@ -1,343 +0,0 @@
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

View File

@@ -1,785 +0,0 @@
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,297 +0,0 @@
# 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 osmocom.utils import b2h
from osmocom.tlv import bertlv_encode_len, bertlv_parse_one
# 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)

View File

@@ -1,239 +0,0 @@
"""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 requests
import logging
from datetime import datetime
import time
from pySim.esim.http_json_api import *
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
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 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.isoformat(data)
class NotificationPointId(ApiParamInteger):
pass
class NotificationPointStatus(ApiParam):
pass
class ResultData(ApiParamBase64):
pass
class Es2PlusApiFunction(JsonHttpApiFunction):
"""Base classs for representing an ES2+ API Function."""
pass
# 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': 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': JsonResponseHeader,
'eid': param.Eid,
'matchingId': param.MatchingId,
'smdpAddress': 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': 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': 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())

View File

@@ -1,291 +0,0 @@
# 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 cryptography.hazmat.primitives.asymmetric import ec
from osmocom.utils import b2h, h2b
from osmocom.tlv import bertlv_encode_tag, bertlv_encode_len, bertlv_parse_one_rawtag
from osmocom.tlv import bertlv_return_one_rawtlv
import pySim.esim.rsp as rsp
from pySim.esim.bsp import BspInstance
from pySim.esim import PMO
# 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
self.icon = None
self.icon_type = None
self.notifications = []
def set_icon(self, is_png: bool, icon_data: bytes):
"""Set the icon that is part of the metadata."""
if len(icon_data) > 1024:
raise ValueError('Icon data must not exceed 1024 bytes')
self.icon = icon_data
if is_png:
self.icon_type = 1
else:
self.icon_type = 0
def add_notification(self, event: str, address: str):
"""Add an 'other' notification to the notification configuration of the metadata"""
self.notifications.append((event, address))
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,
}
if self.icon:
smr['icon'] = self.icon
smr['iconType'] = self.icon_type
nci = []
for n in self.notifications:
pmo = PMO(n[0])
nci.append({'profileManagementOperation': pmo.to_bitstring(), 'notificationAddress': n[1]})
if len(nci):
smr['notificationConfigurationInfo'] = nci
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
def decode(self, euicc_ot, eid: str, bpp_bin: bytes):
"""Decode a BPP into the PPP and subsequently UPP. This is what happens inside an eUICC."""
def split_bertlv_sequence(sequence: bytes) -> List[bytes]:
remainder = sequence
ret = []
while remainder:
_tag, _l, tlv, remainder = bertlv_return_one_rawtlv(remainder)
ret.append(tlv)
return ret
# we don't use rsp.asn1.decode('boundProfilePackage') here, as the BSP needs
# fully encoded + MACed TLVs including their tag + length values.
#bpp = rsp.asn1.decode('BoundProfilePackage', bpp_bin)
tag, _l, v, _remainder = bertlv_parse_one_rawtag(bpp_bin)
if len(_remainder):
raise ValueError('Excess data at end of TLV')
if tag != 0xbf36:
raise ValueError('Unexpected outer tag: %s' % tag)
# InitialiseSecureChannelRequest
tag, _l, iscr_bin, remainder = bertlv_return_one_rawtlv(v)
iscr = rsp.asn1.decode('InitialiseSecureChannelRequest', iscr_bin)
# configureIsdpRequest
tag, _l, firstSeqOf87, remainder = bertlv_parse_one_rawtag(remainder)
if tag != 0xa0:
raise ValueError("Unexpected 'firstSequenceOf87' tag: %s" % tag)
firstSeqOf87 = split_bertlv_sequence(firstSeqOf87)
# storeMetadataRequest
tag, _l, seqOf88, remainder = bertlv_parse_one_rawtag(remainder)
if tag != 0xa1:
raise ValueError("Unexpected 'sequenceOf88' tag: %s" % tag)
seqOf88 = split_bertlv_sequence(seqOf88)
tag, _l, tlv, remainder = bertlv_parse_one_rawtag(remainder)
if tag == 0xa2:
secondSeqOf87 = split_bertlv_sequence(tlv)
tag2, _l, seqOf86, remainder = bertlv_parse_one_rawtag(remainder)
if tag2 != 0xa3:
raise ValueError("Unexpected 'sequenceOf86' tag: %s" % tag)
seqOf86 = split_bertlv_sequence(seqOf86)
elif tag == 0xa3:
secondSeqOf87 = None
seqOf86 = split_bertlv_sequence(tlv)
else:
raise ValueError("Unexpected 'secondSequenceOf87' tag: %s" % tag)
# extract smdoOtpk from initialiseSecureChannel
smdp_otpk = iscr['smdpOtpk']
# Generate Session Keys using the CRT, opPK.DP.ECKA and otSK.EUICC.ECKA according to annex G
smdp_public_key = ec.EllipticCurvePublicKey.from_encoded_point(euicc_ot.curve, smdp_otpk)
self.shared_secret = euicc_ot.exchange(ec.ECDH(), smdp_public_key)
crt = iscr['controlRefTemplate']
bsp = BspInstance.from_kdf(self.shared_secret, int.from_bytes(crt['keyType'], 'big'), int.from_bytes(crt['keyLen'], 'big'), crt['hostId'], h2b(eid))
self.encoded_configureISDPRequest = bsp.demac_and_decrypt(firstSeqOf87)
self.configureISDPRequest = rsp.asn1.decode('ConfigureISDPRequest', self.encoded_configureISDPRequest)
self.encoded_storeMetadataRequest = bsp.demac_only(seqOf88)
self.storeMetadataRequest = rsp.asn1.decode('StoreMetadataRequest', self.encoded_storeMetadataRequest)
if secondSeqOf87 != None:
rsk_bin = bsp.demac_and_decrypt(secondSeqOf87)
rsk = rsp.asn1.decode('ReplaceSessionKeysRequest', rsk_bin)
# process replace_session_keys!
bsp = BspInstance(rsk['ppkEnc'], rsk['ppkCmac'], rsk['initialMacChainingValue'])
self.replaceSessionKeysRequest = rsk
self.upp = bsp.demac_and_decrypt(seqOf86)
return self.upp

View File

@@ -1,177 +0,0 @@
"""GSMA eSIM RSP ES9+ interface according ot 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 requests
import logging
import time
import pySim.esim.rsp as rsp
from pySim.esim.http_json_api import *
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class param:
class RspAsn1Par(ApiParamBase64):
"""Generalized RSP ASN.1 parameter: base64-wrapped ASN.1 DER. Derived classes must provide
the asn1_type class variable to indicate the name of the ASN.1 type to use for encode/decode."""
asn1_type = None # must be overridden by derived class
@classmethod
def _decode(cls, data):
data = ApiParamBase64.decode(data)
return rsp.asn1.decode(cls.asn1_type, data)
@classmethod
def _encode(cls, data):
data = rsp.asn1.encode(cls.asn1_type, data)
return ApiParamBase64.encode(data)
class EuiccInfo1(RspAsn1Par):
asn1_type = 'EUICCInfo1'
class ServerSigned1(RspAsn1Par):
asn1_type = 'ServerSigned1'
class PrepareDownloadResponse(RspAsn1Par):
asn1_type = 'PrepareDownloadResponse'
class AuthenticateServerResponse(RspAsn1Par):
asn1_type = 'AuthenticateServerResponse'
class SmdpSigned2(RspAsn1Par):
asn1_type = 'SmdpSigned2'
class StoreMetadataRequest(RspAsn1Par):
asn1_type = 'StoreMetadataRequest'
class PendingNotification(RspAsn1Par):
asn1_type = 'PendingNotification'
class CancelSessionResponse(RspAsn1Par):
asn1_type = 'CancelSessionResponse'
class TransactionId(ApiParamString):
pass
class Es9PlusApiFunction(JsonHttpApiFunction):
pass
# ES9+ InitiateAuthentication function (SGP.22 section 6.5.2.6)
class InitiateAuthentication(Es9PlusApiFunction):
path = '/gsma/rsp2/es9plus/initiateAuthentication'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'euiccChallenge': ApiParamBase64,
'euiccInfo1': param.EuiccInfo1,
'smdpAddress': SmdpAddress,
}
input_mandatory = ['euiccChallenge', 'euiccInfo1', 'smdpAddress']
output_params = {
'header': JsonResponseHeader,
'transactionId': param.TransactionId,
'serverSigned1': param.ServerSigned1,
'serverSignature1': ApiParamBase64,
'euiccCiPKIdToBeUsed': ApiParamBase64,
'serverCertificate': ApiParamBase64,
}
output_mandatory = ['header', 'transactionId', 'serverSigned1', 'serverSignature1',
'euiccCiPKIdToBeUsed', 'serverCertificate']
# ES9+ GetBoundProfilePackage function (SGP.22 section 6.5.2.7)
class GetBoundProfilePackage(Es9PlusApiFunction):
path = '/gsma/rsp2/es9plus/getBoundProfilePackage'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'transactionId': param.TransactionId,
'prepareDownloadResponse': param.PrepareDownloadResponse,
}
input_mandatory = ['transactionId', 'prepareDownloadResponse']
output_params = {
'header': JsonResponseHeader,
'transactionId': param.TransactionId,
'boundProfilePackage': ApiParamBase64,
}
output_mandatory = ['header', 'transactionId', 'boundProfilePackage']
# ES9+ AuthenticateClient function (SGP.22 section 6.5.2.8)
class AuthenticateClient(Es9PlusApiFunction):
path= '/gsma/rsp2/es9plus/authenticateClient'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'transactionId': param.TransactionId,
'authenticateServerResponse': param.AuthenticateServerResponse,
}
input_mandatory = ['transactionId', 'authenticateServerResponse']
output_params = {
'header': JsonResponseHeader,
'transactionId': param.TransactionId,
'profileMetadata': param.StoreMetadataRequest,
'smdpSigned2': param.SmdpSigned2,
'smdpSignature2': ApiParamBase64,
'smdpCertificate': ApiParamBase64,
}
output_mandatory = ['header', 'transactionId', 'profileMetadata', 'smdpSigned2',
'smdpSignature2', 'smdpCertificate']
# ES9+ HandleNotification function (SGP.22 section 6.5.2.9)
class HandleNotification(Es9PlusApiFunction):
path = '/gsma/rsp2/es9plus/handleNotification'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'pendingNotification': param.PendingNotification,
}
input_mandatory = ['pendingNotification']
expected_http_status = 204
# ES9+ CancelSession function (SGP.22 section 6.5.2.10)
class CancelSession(Es9PlusApiFunction):
path = '/gsma/rsp2/es9plus/cancelSession'
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
input_params = {
'transactionId': param.TransactionId,
'cancelSessionResponse': param.CancelSessionResponse,
}
input_mandatory = ['transactionId', 'cancelSessionResponse']
class Es9pApiClient:
def __init__(self, url_prefix:str, server_cert_verify: str = None):
self.session = requests.Session()
self.session.verify = False # FIXME HACK
if server_cert_verify:
self.session.verify = server_cert_verify
self.initiateAuthentication = InitiateAuthentication(url_prefix, '', self.session)
self.authenticateClient = AuthenticateClient(url_prefix, '', self.session)
self.getBoundProfilePackage = GetBoundProfilePackage(url_prefix, '', self.session)
self.handleNotification = HandleNotification(url_prefix, '', self.session)
self.cancelSession = CancelSession(url_prefix, '', self.session)
def call_initiateAuthentication(self, data: dict) -> dict:
return self.initiateAuthentication.call(data)
def call_authenticateClient(self, data: dict) -> dict:
return self.authenticateClient.call(data)
def call_getBoundProfilePackage(self, data: dict) -> dict:
return self.getBoundProfilePackage.call(data)
def call_handleNotification(self, data: dict) -> dict:
return self.handleNotification.call(data)
def call_cancelSession(self, data: dict) -> dict:
return self.cancelSession.call(data)

View File

@@ -1,259 +0,0 @@
"""GSMA eSIM RSP HTTP/REST/JSON 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 typing import Optional
import base64
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class ApiParam(abc.ABC):
"""A class reprsenting a single parameter in the 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 ApiParamBase64(ApiParam):
@classmethod
def _decode(cls, data):
return base64.b64decode(data)
@classmethod
def _encode(cls, data):
return base64.b64encode(data).decode('ascii')
class SmdpAddress(ApiParamFqdn):
pass
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 ApiError(Exception):
"""Exception representing an error at the 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 JsonHttpApiFunction(abc.ABC):
"""Base classs for representing an HTTP[s] 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'
extra_http_req_headers = {}
def __init__(self, url_prefix: str, func_req_id: Optional[str], session: requests.Session):
self.url_prefix = url_prefix
self.func_req_id = func_req_id
self.session = session
def encode(self, data: dict, func_call_id: Optional[str] = None) -> dict:
"""Validate an encode input dict into JSON-serializable dict for request body."""
output = {}
if func_call_id:
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 response body."""
output = {}
if 'header' in self.output_params:
# 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 ApiError(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: Optional[str] = None, timeout=10) -> Optional[dict]:
"""Make an API call to the HTTP API endpoint represented by this object.
Input data is passed in `data` as json-serializable dict. Output data
is returned as json-deserialized dict."""
url = self.url_prefix + self.path
encoded = json.dumps(self.encode(data, func_call_id))
req_headers = {
'Content-Type': 'application/json',
'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
}
req_headers.update(self.extra_http_req_headers)
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
response = self.session.request(self.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
logger.debug("HTTP RSP: %s" % (response.content))
if response.status_code != self.expected_http_status:
raise HttpStatusError(response)
if not response.headers.get('Content-Type').startswith(req_headers['Content-Type']):
raise HttpHeaderError(response)
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
raise HttpHeaderError(response)
if response.content:
return self.decode(response.json())
return None

View File

@@ -1,132 +0,0 @@
# 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 osmocom.utils import b2h
from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,120 +0,0 @@
# 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 functools import total_ordering
from typing import List, Union
@total_ordering
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])
@staticmethod
def highest_oid(oids: List['OID']) -> 'OID':
return sorted(oids)[-1]
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))
def __eq__(self, other: 'OID'):
return (self.intlist == other.intlist)
def __ne__(self, other: 'OID'):
# implement based on __eq__
return not (self == other)
def cmp(self, other: 'OID'):
self_len = len(self.intlist)
other_len = len(other.intlist)
common_len = min(self_len, other_len)
max_len = max(self_len, other_len)
for i in range(0, max_len+1):
if i >= self_len:
# other list is longer
return -1
if i >= other_len:
# our list is longer
return 1
if self.intlist[i] > other.intlist[i]:
# our version is higher
return 1
if self.intlist[i] < other.intlist[i]:
# other version is higher
return -1
# continue to next digit
return 0
def __gt__(self, other: 'OID'):
if self.cmp(other) > 0:
return True
def prefix_match(self, oid_str: Union[str, 'OID']):
"""determine if oid_str is equal or below our OID."""
return str(oid_str).startswith(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_USIMopt_not_by_default = eOID("2.5")
ADF_USIMopt_not_by_default_v2 = eOID("2.5.2")
ADF_USIMopt_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_ISIMopt_not_by_default = eOID("2.9")
ADF_ISIMopt_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_CSIMopt_not_by_default = eOID("2.11")
ADF_CSIMopt_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_by_default = eOID("2.17")
IoTopt_not_by_default = eOID("2.18")

View File

@@ -1,324 +0,0 @@
# 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 osmocom.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

View File

@@ -1,974 +0,0 @@
# 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
from pySim.utils import all_subclasses, h2b
from pySim.filesystem import Path
import pySim.esim.saip.oid as OID
class FileTemplate:
"""Representation of a single file in a SimAlliance/TCA Profile Template. The argument order
is done to match that of the tables in Section 9 of the SAIP specification."""
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, repeat:bool = False, ppath: List[int] = []):
"""
Args:
fid: The 16bit file-identifier of the file
name: The name of the file in human-readable "EF.FOO", "DF.BAR" notation
ftype: The type of the file; can be 'MF', 'ADF', 'DF', 'TR', 'LF', 'CY', 'BT'
nb_rec: Then number of records (only valid for 'LF' and 'CY')
size: The size of the file ('TR', 'BT'); size of each record ('LF, 'CY')
arr: The record number of EF.ARR for referenced access rules
sfi: The short file identifier, if any
default_val: The default value [pattern] of the file
content_rqd: Whether an instance of template *must* specify file contents
params: A list of parameters that an instance of the template *must* specify
ass_serv: The associated service[s] of the service table
high_update: Is this file of "high update frequency" type?
pe_name: The name of this file in the ASN.1 type of the PE. Auto-generated for most.
repeat: Whether the default_val pattern is a repeating pattern.
ppath: The intermediate path between the base_df of the ProfileTemplate and this file. If not
specified, the file will be created immediately underneath the base_df.
"""
# 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', 'BT']:
self.file_size = size
self.arr = arr
self.sfi = sfi
self.default_val = default_val
self.default_val_repeat = repeat
self.content_rqd = content_rqd
self.params = params
self.ass_serv = ass_serv
self.high_update = high_update
self.ppath = ppath # parent path, if this FileTemplate is not immediately below the base_df
# initialize empty
self.parent = None
self.children = []
if self.default_val:
length = self._default_value_len() or 100
# run the method once to verify the pattern can be processed
self.expand_default_value_pattern(length)
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, ppath=%s)" % (self.name, self.pe_name, s_fid, self.file_type, s_arr, s_sfi, self.ppath)
def print_tree(self, indent:str = ""):
"""recursive printing of FileTemplate tree structure."""
print("%s%s (%s)" % (indent, repr(self), self.path))
indent += " "
for c in self.children:
c.print_tree(indent)
@property
def path(self):
"""Return the path of the given File within the hierarchy."""
if self.parent:
return self.parent.path + self.name
else:
return Path(self.name)
def get_file_by_path(self, path: List[str]) -> Optional['FileTemplate']:
"""Return a FileTemplate matching the given path within this ProfileTemplate."""
if path[0].lower() != self.name.lower():
return None
for c in self.children:
if path[1].lower() == c.name.lower():
return c.get_file_by_path(path[1:])
def _default_value_len(self):
if self.file_type in ['TR']:
return self.file_size
elif self.file_type in ['LF', 'CY']:
return self.rec_len
def expand_default_value_pattern(self, length: Optional[int] = None) -> Optional[bytes]:
"""Expand the default value pattern to the specified length."""
if length is None:
length = self._default_value_len()
if length is None:
raise ValueError("%s does not have a default length" % self)
if not self.default_val:
return None
if not '...' in self.default_val:
return h2b(self.default_val)
l = self.default_val.split('...')
if len(l) != 2:
raise ValueError("Pattern '%s' contains more than one ..." % self.default_val)
prefix = h2b(l[0])
suffix = h2b(l[1])
pad_len = length - len(prefix) - len(suffix)
if pad_len <= 0:
ret = prefix + suffix
return ret[:length]
return prefix + prefix[-1:] * pad_len + suffix
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
optional: bool = False
oid: Optional[OID.eOID] = None
files: List[FileTemplate] = []
# indicates that a given template does not have its own 'base DF', but that its contents merely
# extends that of the 'base DF' of another template
extends: Optional['ProfileTemplate'] = None
# indicates a parent ProfileTemplate below whose 'base DF' our files should be placed.
parent: Optional['ProfileTemplate'] = None
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)
cur_df = None
cls.files_by_pename: dict[str,FileTemplate] = {}
cls.tree: List[FileTemplate] = []
if not cls.optional and not cls.files[0].file_type in ['MF', 'DF', 'ADF']:
raise ValueError('First file in non-optional template must be MF, DF or ADF (is: %s)' % cls.files[0])
for f in cls.files:
if f.file_type in ['MF', 'DF', 'ADF']:
if cur_df == None:
cls.tree.append(f)
f.parent = None
cur_df = f
else:
# "cd .."
if cur_df.parent:
cur_df = cur_df.parent
f.parent = cur_df
cur_df.children.append(f)
cur_df = f
else:
if cur_df == None:
cls.tree.append(f)
f.parent = None
else:
cur_df.children.append(f)
f.parent = cur_df
cls.files_by_pename[f.pe_name] = f
ProfileTemplateRegistry.add(cls)
@classmethod
def print_tree(cls):
for c in cls.tree:
c.print_tree()
@classmethod
def base_df(cls) -> FileTemplate:
"""Return the FileTemplate for the base DF of the given template. This may be a DF or ADF
within this template, or refer to another template (e.g. mandatory USIM if we are optional USIM."""
if cls.extends:
return cls.extends.base_df
return cls.files[0]
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.3.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'], ppath=[0x5f3a]),
]
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
df_pb_files += [
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi'], ppath=[0x5f3a]),
FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True, ppath=[0x5f3a]),
FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True, ppath=[0x5f3a]),
]
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
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'], ppath=[0x5f3a]))
# Section 9.4 v2.3.1
class FilesTelecom(ProfileTemplate):
created_by_default = False
oid = OID.DF_TELECOM
base_path = Path('MF')
files = [
FileTemplate(0x7f10, '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'], ppath=[0x5f50]),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
# 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'], ppath=[0x5f50]))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'], ppath=[0x5f50]))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
df_pb = deepcopy(df_pb_files)
files += df_pb
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], ppath=[0x5f3b]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], ppath=[0x5f3c]),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size'], ppath=[0x5f3c]),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True, ppath=[0x5f3c]),
]
# Section 9.4
class FilesTelecomV2(ProfileTemplate):
created_by_default = False
oid = OID.DF_TELECOM_v2
base_path = Path('MF')
files = [
FileTemplate(0x7f10, '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'], ppath=[0x5f50]),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
# 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'], ppath=[0x5f50]))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'],ppath=[0x5f50]))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
df_pb = deepcopy(df_pb_files)
files += df_pb
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], ppath=[0x5f3b]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], ppath=[0x5f3c]),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size'], ppath=[0x5f3c]),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True, ppath=[0x5f3c]),
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}, ppath=[0x5f3d]),
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
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], ppath=[0x5f3e]),
FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]),
FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]), # VST: 2
FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]), # 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
optional = True
oid = OID.ADF_USIMopt_not_by_default
base_path = Path('ADF.USIM')
extends = FilesUsimMandatory
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', False, ass_serv=[20], repeat=True),
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000', False, ass_serv=[42], repeat=True),
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43], repeat=True),
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
optional = True
oid = OID.ADF_USIMopt_not_by_default_v2
base_path = Path('ADF.USIM')
extends = FilesUsimMandatoryV2
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.2.3 v3.3.1
class FilesUsimOptionalV3(ProfileTemplate):
created_by_default = False
optional = True
oid = OID.ADF_USIMopt_not_by_default_v3
base_path = Path('ADF.USIM')
extends = FilesUsimMandatoryV2
files = FilesUsimOptionalV2.files + [
FileTemplate(0x6f01, 'EF.eAKA', 'TR', None, 1, 3, None, None, True, ['size'], ass_serv=[134]),
]
# Section 9.5.3
class FilesUsimDfPhonebook(ProfileTemplate):
created_by_default = False
oid = OID.DF_PHONEBOOK_ADF_USIM
base_path = Path('ADF.USIM')
files = df_pb_files
# Section 9.5.4
class FilesUsimDfGsmAccess(ProfileTemplate):
created_by_default = False
oid = OID.DF_GSM_ACCESS_ADF_USIM
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
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
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
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
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
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
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
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.11.4
class FilesUsimDf5GSv4(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GS_v4
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
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, 'F0FF0000', 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]),
FileTemplate(0x4f0d, 'EF.CAG', 'TR', None, 2, 2, 0x0d, None, True, ass_serv=[137]),
FileTemplate(0x4f0e, 'EF.SOR_CMCI', 'TR', None, None, 2, 0x0e, None, True, ass_serv=[138]),
FileTemplate(0x4f0f, 'EF.DRI', 'TR', None, 7, 2, 0x0f, None, True, ass_serv=[150]),
FileTemplate(0x4f10, 'EF.5GSEDRX', 'TR', None, 2, 2, 0x10, None, True, ass_serv=[141]),
FileTemplate(0x4f11, 'EF.5GNSWO_CONF', 'TR', None, 1, 2, 0x11, None, True, ass_serv=[142]),
FileTemplate(0x4f15, 'EF.MCHPPLMN', 'TR', None, 1, 2, 0x15, None, True, ass_serv=[144]),
FileTemplate(0x4f16, 'EF.KAUSF_DERIVATION', 'TR', None, 1, 2, 0x16, None, True, ass_serv=[145]),
]
# Section 9.5.12
class FilesUsimDfSaip(ProfileTemplate):
created_by_default = False
oid = OID.DF_SAIP
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
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.5.13
class FilesDfSnpn(ProfileTemplate):
created_by_default = False
oid = OID.DF_SNPN
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x5fe0, 'DF.SNPN', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[143], pe_name='df-df-snpn'),
FileTemplate(0x4f01, 'EF.PWS_SNPN', 'TR', None, 1, 10, None, None, True, ass_serv=[143]),
]
# Section 9.5.14
class FilesDf5GProSe(ProfileTemplate):
created_by_default = False
oid = OID.DF_5GProSe
base_path = Path('ADF.USIM')
parent = FilesUsimMandatory
files = [
FileTemplate(0x5ff0, 'DF.5G_ProSe', 'DF', None, None, 14, None, None, False, ['pinStatusTeimplateDO'], ass_serv=[139], pe_name='df-df-5g-prose'),
FileTemplate(0x4f01, 'EF.5G_PROSE_ST', 'TR', None, 1, 2, 0x01, None, True, ass_serv=[139]),
FileTemplate(0x4f02, 'EF.5G_PROSE_DD', 'TR', None, 26, 2, 0x02, None, True, ass_serv=[139,1001]),
FileTemplate(0x4f03, 'EF.5G_PROSE_DC', 'TR', None, 12, 2, 0x03, None, True, ass_serv=[139,1002]),
FileTemplate(0x4f04, 'EF.5G_PROSE_U2NRU', 'TR', None, 32, 2, 0x04, None, True, ass_serv=[139,1003]),
FileTemplate(0x4f05, 'EF.5G_PROSE_RU', 'TR', None, 29, 2, 0x05, None, True, ass_serv=[139,1004]),
FileTemplate(0x4f06, 'EF.5G_PROSE_UIR', 'TR', None, 32, 2, 0x06, None, True, ass_serv=[139,1005]),
]
# 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
optional = True
oid = OID.ADF_ISIMopt_not_by_default
base_path = Path('ADF.ISIM')
extends = FilesIsimMandatory
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
optional = True
oid = OID.ADF_ISIMopt_not_by_default_v2
base_path = Path('ADF.ISIM')
extends = FilesIsimMandatory
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'],
}
class SaipSpecVersionMeta(type):
def __getitem__(self, ver: str):
"""Syntactic sugar so that SaipSpecVersion['2.3.0'] will work."""
return SaipSpecVersion.for_version(ver)
class SaipSpecVersion(object, metaclass=SaipSpecVersionMeta):
"""Represents a specific version of the SIMalliance / TCA eUICC Profile Package:
Interoperable Format Technical Specification."""
version = None
oids = []
@classmethod
def suports_template_OID(cls, OID: OID.OID) -> bool:
"""Return if a given spec version supports a template of given OID."""
return OID in cls.oids
@classmethod
def version_match(cls, ver: str) -> bool:
"""Check if the given version-string matches the classes version. trailing zeroes are ignored,
so that for example 2.2.0 will be considered equal to 2.2"""
def strip_trailing_zeroes(l: List):
while l[-1] == '0':
l.pop()
cls_ver_l = cls.version.split('.')
strip_trailing_zeroes(cls_ver_l)
ver_l = ver.split('.')
strip_trailing_zeroes(ver_l)
return cls_ver_l == ver_l
@staticmethod
def for_version(req_version: str) -> Optional['SaipSpecVersion']:
"""Return the subclass for the requested version number string."""
for cls in all_subclasses(SaipSpecVersion):
if cls.version_match(req_version):
return cls
class SaipSpecVersion101(SaipSpecVersion):
version = '1.0.1'
oids = [OID.MF, OID.DF_CD, OID.DF_TELECOM, OID.ADF_USIM_by_default, OID.ADF_USIMopt_not_by_default,
OID.DF_PHONEBOOK_ADF_USIM, OID.DF_GSM_ACCESS_ADF_USIM, OID.ADF_ISIM_by_default,
OID.ADF_ISIMopt_not_by_default, OID.ADF_CSIM_by_default, OID.ADF_CSIMopt_not_by_default]
class SaipSpecVersion20(SaipSpecVersion):
version = '2.0'
# no changes in filesystem teplates to previous 1.0.1
oids = SaipSpecVersion101.oids
class SaipSpecVersion21(SaipSpecVersion):
version = '2.1'
# no changes in filesystem teplates to previous 2.0
oids = SaipSpecVersion20.oids
class SaipSpecVersion22(SaipSpecVersion):
version = '2.2'
oids = SaipSpecVersion21.oids + [OID.DF_EAP]
class SaipSpecVersion23(SaipSpecVersion):
version = '2.3'
oids = SaipSpecVersion22.oids + [OID.DF_5GS, OID.DF_SAIP]
class SaipSpecVersion231(SaipSpecVersion):
version = '2.3.1'
# no changes in filesystem teplates to previous 2.3
oids = SaipSpecVersion23.oids
class SaipSpecVersion31(SaipSpecVersion):
version = '3.1'
oids = [OID.MF, OID.DF_CD, OID.DF_TELECOM_v2, OID.ADF_USIM_by_default_v2, OID.ADF_USIMopt_not_by_default_v2,
OID.DF_PHONEBOOK_ADF_USIM, OID.DF_GSM_ACCESS_ADF_USIM, OID.DF_5GS_v2, OID.DF_5GS_v3, OID.DF_SAIP,
OID.ADF_ISIM_by_default, OID.ADF_ISIMopt_not_by_default_v2, OID.ADF_CSIM_by_default_v2,
OID.ADF_CSIMopt_not_by_default_v2, OID.DF_EAP]
class SaipSpecVersion32(SaipSpecVersion):
version = '3.2'
# no changes in filesystem teplates to previous 3.1
oids = SaipSpecVersion31.oids
class SaipSpecVersion331(SaipSpecVersion):
version = '3.3.1'
oids = SaipSpecVersion32.oids + [OID.ADF_USIMopt_not_by_default_v3, OID.DF_5GS_v4, OID.DF_SAIP, OID.DF_SNPN, OID.DF_5GProSe, OID.IoT_by_default, OID.IoTopt_not_by_default]

View File

@@ -1,134 +0,0 @@
# 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."""

View File

@@ -1,210 +0,0 @@
# 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

View File

@@ -1,558 +0,0 @@
# -*- 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 osmocom.utils import Hexstr
from osmocom.tlv import *
from osmocom.construct import *
from pySim.exceptions import SwMatchError
from pySim.utils import Hexstr, SwHexstr, SwMatchstr
from pySim.commands import SimCardCommands
import pySim.global_platform
# SGP.02 Section 2.2.2
class Sgp02Eid(BER_TLV_IE, tag=0x5a):
_construct = BcdAdapter(GreedyBytes)
# patch this into global_platform, to allow 'get_data sgp02_eid' in EF.ECASD
pySim.global_platform.DataCollection.possible_nested.append(Sgp02Eid)
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 = Asn1DerInteger()
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 = Asn1DerInteger()
class AssociationToken(BER_TLV_IE, tag=0x84):
_construct = Asn1DerInteger()
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 = '80E29100%02x%s00' % (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('--profile-nickname', help='Nickname of the profile')
set_nickname_parser.add_argument('ICCID', help='ICCID of the profile whose nickname to set')
@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

View File

@@ -24,14 +24,17 @@
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):
@@ -49,18 +52,9 @@ class SwMatchError(Exception):
self.sw_expected = sw_expected
self.rs = rs
@property
def description(self):
def __str__(self):
if self.rs and self.rs.lchan[0]:
r = self.rs.lchan[0].interpret_sw(self.sw_actual)
if r:
return "%s - %s" % (r[0], r[1])
return ''
def __str__(self):
description = self.description
if description:
description = ": " + description
else:
description = "."
return "SW match failed! Expected %s and got %s%s" % (self.sw_expected, self.sw_actual, description)
return "SW match failed! Expected %s and got %s: %s - %s" % (self.sw_expected, self.sw_actual, r[0], r[1])
return "SW match failed! Expected %s and got %s." % (self.sw_expected, self.sw_actual)

File diff suppressed because it is too large Load Diff

256
pySim/global_platform.py Normal file
View File

@@ -0,0 +1,256 @@
# 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)

View File

@@ -1,954 +0,0 @@
# 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 osmocom.utils import *
from osmocom.tlv import *
from osmocom.construct import *
from pySim.utils import ResTuple
from pySim.card_key_provider import card_key_provider_get_field
from pySim.global_platform.scp import SCP02, SCP03
from pySim.filesystem import *
from pySim.profile import CardProfile
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
# GPCS Table 11-48 Load Parameter Tags
class NonVolatileCodeMinMemoryReq(BER_TLV_IE, tag=0xC6):
_construct = GreedyInteger()
# GPCS Table 11-48 Load Parameter Tags
class VolatileMinMemoryReq(BER_TLV_IE, tag=0xC7):
_construct = GreedyInteger()
# GPCS Table 11-48 Load Parameter Tags
class NonVolatileDataMinMemoryReq(BER_TLV_IE, tag=0xC8):
_construct = GreedyInteger()
# GPCS Table 11-49: Install Parameter Tags
class GlobalServiceParams(BER_TLV_IE, tag=0xCB):
pass
# GPCS Table 11-49: Install Parameter Tags
class VolatileReservedMemory(BER_TLV_IE, tag=0xD7):
_construct = GreedyInteger()
# GPCS Table 11-49: Install Parameter Tags
class NonVolatileReservedMemory(BER_TLV_IE, tag=0xD8):
_construct = GreedyInteger()
# GPCS Table 11-49: Install Parameter Tags
class Ts102226SpecificParameter(BER_TLV_IE, tag=0xCA):
_construct = SimFileAccessAndToolkitAppSpecParams
# GPCS Table 11-48 Load Parameter Tags
class LoadFileDataBlockFormatId(BER_TLV_IE, tag=0xCD):
pass
# GPCS Table 11-50: Make Selectable Parameter Tags
class ImplicitSelectionParam(BER_TLV_IE, tag=0xCF):
pass
# GPCS Table 11-48 Load Parameter Tags
class LoadFileDtaBlockParameters(BER_TLV_IE, tag=0xDD):
pass
# GPCS Table 11-48 Load Parameter Tags / 11-49: Install Parameter Tags
class SystemSpecificParams(BER_TLV_IE, tag=0xEF,
nested=[NonVolatileCodeMinMemoryReq,
VolatileMinMemoryReq,
NonVolatileDataMinMemoryReq,
GlobalServiceParams,
VolatileReservedMemory,
NonVolatileReservedMemory,
Ts102226SpecificParameter,
LoadFileDataBlockFormatId,
ImplicitSelectionParam,
LoadFileDtaBlockParameters]):
pass
# GPCS Table 11-49: Install Parameter Tags
class ApplicationSpecificParams(BER_TLV_IE, tag=0xC9):
_construct = GreedyBytes
class Ts102226SpecificTemplate(BER_TLV_IE, tag=0xEA):
pass
class CrtForDigitalSignature(BER_TLV_IE, tag=0xB6):
# FIXME: nested
pass
class InstallParameters(TLV_IE_Collection, nested=[ApplicationSpecificParams,
SystemSpecificParams,
Ts102226SpecificTemplate,
CrtForDigitalSignature]):
pass
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) + "00")
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=list(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) + "00")
return data
get_status_parser = argparse.ArgumentParser()
get_status_parser.add_argument('subset', choices=list(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) + "00")
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=list(SetStatusScope.ksymapping.values()),
help='Defines the scope of the requested status change')
set_status_parser.add_argument('status', choices=list(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=list(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%s00" % (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.description = None
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')
self.adf.scp_key_identity = 'ICCID'
#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]

View File

@@ -1,93 +0,0 @@
"""GlobalPlatform Remote Application Management over HTTP Card Specification v2.3 - Amendment B.
Also known as SCP81 for SIM/USIM/UICC/eUICC/eSIM OTA.
"""
# (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 Struct, Int8ub, Int16ub, Bytes, GreedyBytes, GreedyString, BytesInteger
from construct import this, len_, Rebuild, Const
from construct import Optional as COptional
from osmocom.tlv import BER_TLV_IE
from pySim import cat
# Table 3-3 + Section 3.8.1
class RasConnectionParams(BER_TLV_IE, tag=0x84, nested=cat.OpenChannel.nested_collection_cls.possible_nested):
pass
# Table 3-3 + Section 3.8.2
class SecurityParams(BER_TLV_IE, tag=0x85):
_test_de_encode = [
( '850804deadbeef020040', {'kid': 64,'kvn': 0, 'psk_id': b'\xde\xad\xbe\xef', 'sha_type': None} )
]
_construct = Struct('_psk_id_len'/Rebuild(Int8ub, len_(this.psk_id)), 'psk_id'/Bytes(this._psk_id_len),
'_kid_kvn_len'/Const(2, Int8ub), 'kvn'/Int8ub, 'kid'/Int8ub,
'sha_type'/COptional(Int8ub))
# Table 3-3 + ?
class ExtendedSecurityParams(BER_TLV_IE, tag=0xA5):
_construct = GreedyBytes
# Table 3-3 + Section 3.8.3
class SessionRetryPolicyParams(BER_TLV_IE, tag=0x86):
_construct = Struct('retry_counter'/Int16ub,
'retry_waiting_delay'/BytesInteger(5),
'retry_report_failure'/COptional(GreedyBytes))
# Table 3-3 + Section 3.8.4
class AdminHostParam(BER_TLV_IE, tag=0x8A):
_test_de_encode = [
( '8a0a61646d696e2e686f7374', 'admin.host' ),
]
_construct = GreedyString('utf-8')
# Table 3-3 + Section 3.8.5
class AgentIdParam(BER_TLV_IE, tag=0x8B):
_construct = GreedyString('utf-8')
# Table 3-3 + Section 3.8.6
class AdminUriParam(BER_TLV_IE, tag=0x8C):
_test_de_encode = [
( '8c1668747470733a2f2f61646d696e2e686f73742f757269', 'https://admin.host/uri' ),
]
_construct = GreedyString('utf-8')
# Table 3-3
class HttpPostParams(BER_TLV_IE, tag=0x89, nested=[AdminHostParam, AgentIdParam, AdminUriParam]):
pass
# Table 3-3
class AdmSessionParams(BER_TLV_IE, tag=0x83, nested=[RasConnectionParams, SecurityParams,
ExtendedSecurityParams, SessionRetryPolicyParams,
HttpPostParams]):
pass
# Table 3-3 + Section 3.11.4
class RasFqdn(BER_TLV_IE, tag=0xD6):
_construct = GreedyBytes # FIXME: DNS String
# Table 3-3 + Section 3.11.7
class DnsConnectionParams(BER_TLV_IE, tag=0xFA, nested=cat.OpenChannel.nested_collection_cls.possible_nested):
pass
# Table 3-3
class DnsResolutionParams(BER_TLV_IE, tag=0xB3, nested=[RasFqdn, DnsConnectionParams]):
pass
# Table 3-3
class AdmSessTriggerParams(BER_TLV_IE, tag=0x81, nested=[AdmSessionParams, DnsResolutionParams]):
pass

View File

@@ -1,572 +0,0 @@
# 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 osmocom.utils import b2h
from osmocom.tlv import bertlv_parse_len, bertlv_encode_len
from pySim.utils import parse_command_apdu
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]))
elif hasattr(self, 'kvn_ranges'):
# pylint: disable=no-member
if all([not card_keys.kvn in range(x[0], x[1]+1) for x in self.kvn_ranges]):
raise ValueError('%s cannot be used with KVN outside permitted ranges %s' %
(self.__class__.__name__, self.kvn_ranges))
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))
# The 0x70 is a non-spec special-case of sysmoISIM-SJA2/SJA5 and possibly more sysmocom products
kvn_ranges = [[0x20, 0x2f], [0x70, 0x70]]
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[:8], DES.MODE_ECB)
return cipher.encrypt(plaintext)
def dek_decrypt(self, ciphertext:bytes) -> bytes:
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB)
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 + b'\x00'
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."""
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
if not self.do_cmac:
return apdu
(case, lc, le, data) = parse_command_apdu(apdu)
# TODO: add support for extended length fields.
assert lc <= 256
assert le <= 256
lc &= 0xFF
le &= 0xFF
# CLA without log. channel can be 80 or 00 only
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"
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]) + data)
if self.do_cenc:
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
data = k.encrypt(pad80(data, 8))
lc = len(data)
lc += 8
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
# Since we attach a signature, we will always send some data. This means that if the APDU is of case #4
# or case #2, we must attach an additional Le byte to signal that we expect a response. It is technically
# legal to use 0x00 (=256) as Le byte, even when the caller has specified a different value in the original
# APDU. This is due to the fact that Le always describes the maximum expected length of the response
# (see also ISO/IEC 7816-4, section 5.1). In addition to that, it should also important that depending on
# the configuration of the SCP, the response may also contain a signature that makes the response larger
# than specified in the Le field of the original APDU.
if case == 4 or case == 2:
apdu += b'\x00'
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 counters 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 + b'\x00'
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 SCP03: calculate MAC and encrypt."""
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
if not self.do_cmac:
return apdu
cla = apdu[0]
ins = apdu[1]
p1 = apdu[2]
p2 = apdu[3]
(case, lc, le, cmd_data) = parse_command_apdu(apdu)
# TODO: add support for extended length fields.
assert lc <= 256
assert le <= 256
lc &= 0xFF
le &= 0xFF
if self.do_cenc and not skip_cenc:
if case <= 2:
# 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)
# 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
apdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
cmac = self.sk.calc_cmac(apdu)
apdu += cmac[:self.s_mode]
# See comment in SCP03._wrap_cmd_apdu()
if case == 4 or case == 2:
apdu += b'\x00'
return apdu
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

View File

@@ -1,107 +0,0 @@
# 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 osmocom.construct import *
from osmocom.utils import *
from osmocom.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

View File

@@ -1,10 +1,13 @@
# -*- 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>
@@ -25,13 +28,16 @@ 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 Struct, Bytes, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
from construct import *
from construct import Optional as COptional
from osmocom.construct import *
from pySim.construct import *
import enum
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)
@@ -41,10 +47,10 @@ from pySim.filesystem import *
class FuncNTypeAdapter(Adapter):
def _decode(self, obj, context, path):
bcd = swap_nibbles(b2h(obj))
last_digit = int(bcd[-1], 16)
last_digit = bcd[-1]
return {'functional_number': bcd[:-1],
'presentation_of_only_this_fn': bool(last_digit & 4),
'permanent_fn': bool(last_digit & 8)}
'presentation_of_only_this_fn': last_digit & 4,
'permanent_fn': last_digit & 8}
def _encode(self, obj, context, path):
return 'FIXME'
@@ -52,13 +58,9 @@ class FuncNTypeAdapter(Adapter):
class EF_FN(LinFixedEF):
"""Section 7.2"""
_test_decode = [
( "40315801000010ff01",
{ "functional_number_and_type": { "functional_number": "04138510000001f",
"presentation_of_only_this_fn": True, "permanent_fn": True }, "list_number": 1 } ),
]
def __init__(self):
super().__init__(fid='6ff1', sfid=None, name='EF.FN',
super().__init__(fid='6ff1', sfid=None, name='EF.EN',
desc='Functional numbers', rec_len=(9, 9))
self._construct = Struct('functional_number_and_type'/FuncNTypeAdapter(Bytes(8)),
'list_number'/Int8ub)
@@ -71,15 +73,15 @@ class PlConfAdapter(Adapter):
num = int(obj) & 0x7
if num == 0:
return 'None'
if num == 1:
elif num == 1:
return 4
if num == 2:
elif num == 2:
return 3
if num == 3:
elif num == 3:
return 2
if num == 4:
elif num == 4:
return 1
if num == 5:
elif num == 5:
return 0
def _encode(self, obj, context, path):
@@ -88,13 +90,13 @@ class PlConfAdapter(Adapter):
obj = int(obj)
if obj == 4:
return 1
if obj == 3:
elif obj == 3:
return 2
if obj == 2:
elif obj == 2:
return 3
if obj == 1:
elif obj == 1:
return 4
if obj == 0:
elif obj == 0:
return 5
@@ -105,19 +107,19 @@ class PlCallAdapter(Adapter):
num = int(obj) & 0x7
if num == 0:
return 'None'
if num == 1:
elif num == 1:
return 4
if num == 2:
elif num == 2:
return 3
if num == 3:
elif num == 3:
return 2
if num == 4:
elif num == 4:
return 1
if num == 5:
elif num == 5:
return 0
if num == 6:
elif num == 6:
return 'B'
if num == 7:
elif num == 7:
return 'A'
def _encode(self, obj, context, path):
@@ -125,17 +127,17 @@ class PlCallAdapter(Adapter):
return 0
if obj == 4:
return 1
if obj == 3:
elif obj == 3:
return 2
if obj == 2:
elif obj == 2:
return 3
if obj == 1:
elif obj == 1:
return 4
if obj == 0:
elif obj == 0:
return 5
if obj == 'B':
elif obj == 'B':
return 6
if obj == 'A':
elif obj == 'A':
return 7
@@ -145,12 +147,7 @@ NextTableType = Enum(Byte, decision=0xf0, predefined=0xf1,
class EF_CallconfC(TransparentEF):
"""Section 7.3"""
_test_de_encode = [
( "026121ffffffffffff1e000a040a010253600795792426f0",
{ "pl_conf": 3, "conf_nr": "1612ffffffffffff", "max_rand": 30, "n_ack_max": 10,
"pl_ack": 1, "n_nested_max": 10, "train_emergency_gid": 1, "shunting_emergency_gid": 2,
"imei": "350670599742620f" } ),
]
def __init__(self):
super().__init__(fid='6ff2', sfid=None, name='EF.CallconfC', size=(24, 24),
desc='Call Configuration of emergency calls Configuration')
@@ -183,9 +180,7 @@ class EF_CallconfI(LinFixedEF):
class EF_Shunting(TransparentEF):
"""Section 7.6"""
_test_de_encode = [
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": "f8ffffff000000" } ),
]
def __init__(self):
super().__init__(fid='6ff4', sfid=None,
name='EF.Shunting', desc='Shunting', size=(8, 8))
@@ -195,22 +190,11 @@ class EF_Shunting(TransparentEF):
class EF_GsmrPLMN(LinFixedEF):
"""Section 7.7"""
_test_de_encode = [
( "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": "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",
"ic_table_ref": "02" } ),
]
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'/PlmnAdapter(Bytes(3)),
self._construct = Struct('plmn'/BcdAdapter(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)),
@@ -220,29 +204,19 @@ class EF_GsmrPLMN(LinFixedEF):
class EF_IC(LinFixedEF):
"""Section 7.8"""
_test_de_encode = [
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": "6f8e",
"ic_decision_value": "041f", "network_string_table_index": 1 } ),
( "ffffffffffffff", { "next_table_type": "empty", "id_of_next_table": "ffff",
"ic_decision_value": "ffff", "network_string_table_index": 65535 } ),
]
def __init__(self):
super().__init__(fid='6f8d', sfid=None, name='EF.IC',
desc='International Code', rec_len=(7, 7))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/HexAdapter(Bytes(2)),
'ic_decision_value'/BcdAdapter(Bytes(2)),
'network_string_table_index'/Int16ub)
'network_string_table_index'/Int8ub)
class EF_NW(LinFixedEF):
"""Section 7.9"""
_test_de_encode = [
( "47534d2d52204348", "GSM-R CH" ),
( "537769737347534d", "SwissGSM" ),
( "47534d2d52204442", "GSM-R DB" ),
( "47534d2d52524649", "GSM-RRFI" ),
]
def __init__(self):
super().__init__(fid='6f80', sfid=None, name='EF.NW',
desc='Network Name', rec_len=(8, 8))
@@ -251,15 +225,8 @@ class EF_NW(LinFixedEF):
class EF_Switching(LinFixedEF):
"""Section 8.4"""
_test_de_encode = [
( "f26f87f0ff00", { "next_table_type": "num_dial_digits", "id_of_next_table": "6f87",
"decision_value": "0fff", "string_table_index": 0 } ),
( "f06f8ff1ff01", { "next_table_type": "decision", "id_of_next_table": "6f8f",
"decision_value": "1fff", "string_table_index": 1 } ),
( "f16f89f5ff05", { "next_table_type": "predefined", "id_of_next_table": "6f89",
"decision_value": "5fff", "string_table_index": 5 } ),
]
def __init__(self, fid='1234', name='Switching', desc=None):
def __init__(self, fid, name, desc):
super().__init__(fid=fid, sfid=None,
name=name, desc=desc, rec_len=(6, 6))
self._construct = Struct('next_table_type'/NextTableType,
@@ -270,41 +237,22 @@ class EF_Switching(LinFixedEF):
class EF_Predefined(LinFixedEF):
"""Section 8.5"""
_test_de_encode = [
( "f26f85", 1, { "next_table_type": "num_dial_digits", "id_of_next_table": "6f85" } ),
( "f0ffc8", 2, { "predefined_value1": "0fff", "string_table_index1": 200 } ),
]
# header and other records have different structure. WTF !?!
construct_first = Struct('next_table_type'/NextTableType,
'id_of_next_table'/HexAdapter(Bytes(2)))
construct_others = Struct('predefined_value1'/BcdAdapter(Bytes(2)),
'string_table_index1'/Int8ub)
def __init__(self, fid='1234', name='Predefined', desc=None):
def __init__(self, fid, name, desc):
super().__init__(fid=fid, sfid=None,
name=name, desc=desc, rec_len=(3, 3))
# header and other records have different structure. WTF !?!
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/HexAdapter(Bytes(2)),
'predefined_value1'/HexAdapter(Bytes(2)),
'string_table_index1'/Int8ub)
# TODO: predefined value n, ...
def _decode_record_bin(self, raw_bin_data : bytes, record_nr : int) -> dict:
if record_nr == 1:
return parse_construct(self.construct_first, raw_bin_data)
else:
return parse_construct(self.construct_others, raw_bin_data)
def _encode_record_bin(self, abstract_data : dict, record_nr : int, **kwargs) -> bytearray:
r = None
if record_nr == 1:
r = self.construct_first.build(abstract_data)
else:
r = self.construct_others.build(abstract_data)
return filter_dict(r)
class EF_DialledVals(TransparentEF):
"""Section 8.6"""
_test_de_encode = [
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": "ffff", "dialed_digits": "22" } ),
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": "6f88", "dialed_digits": "58" }),
]
def __init__(self, fid='1234', name='DialledVals', desc=None):
def __init__(self, fid, name, desc):
super().__init__(fid=fid, sfid=None, name=name, desc=desc, size=(4, 4))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/HexAdapter(Bytes(2)),
@@ -356,15 +304,3 @@ 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)

214
pySim/gsmtap.py Normal file
View File

@@ -0,0 +1,214 @@
# -*- coding: utf-8 -*-
""" Osmocom GSMTAP python implementation.
GSMTAP is a packet format used for conveying a number of different
telecom-related protocol traces over UDP.
"""
#
# Copyright (C) 2022 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
# 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 socket
from typing import List, Dict, Optional
from construct import Optional as COptional
from construct import *
from pySim.construct import *
# The root definition of GSMTAP can be found at
# https://cgit.osmocom.org/cgit/libosmocore/tree/include/osmocom/core/gsmtap.h
GSMTAP_UDP_PORT = 4729
# GSMTAP_TYPE_*
gsmtap_type_construct = Enum(Int8ub,
gsm_um = 0x01,
gsm_abis = 0x02,
gsm_um_burst = 0x03,
sim = 0x04,
tetra_i1 = 0x05,
tetra_i1_burst = 0x06,
wimax_burst = 0x07,
gprs_gb_llc = 0x08,
gprs_gb_sndcp = 0x09,
gmr1_um = 0x0a,
umts_rlc_mac = 0x0b,
umts_rrc = 0x0c,
lte_rrc = 0x0d,
lte_mac = 0x0e,
lte_mac_framed = 0x0f,
osmocore_log = 0x10,
qc_diag = 0x11,
lte_nas = 0x12,
e1_t1 = 0x13)
# TYPE_UM_BURST
gsmtap_subtype_burst_construct = Enum(Int8ub,
unknown = 0x00,
fcch = 0x01,
partial_sch = 0x02,
sch = 0x03,
cts_sch = 0x04,
compact_sch = 0x05,
normal = 0x06,
dummy = 0x07,
access = 0x08,
none = 0x09)
gsmtap_subtype_wimax_burst_construct = Enum(Int8ub,
cdma_code = 0x10,
fch = 0x11,
ffb = 0x12,
pdu = 0x13,
hack = 0x14,
phy_attributes = 0x15)
# GSMTAP_CHANNEL_*
gsmtap_subtype_um_construct = Enum(Int8ub,
unknown = 0x00,
bcch = 0x01,
ccch = 0x02,
rach = 0x03,
agch = 0x04,
pch = 0x05,
sdcch = 0x06,
sdcch4 = 0x07,
sdcch8 = 0x08,
facch_f = 0x09,
facch_h = 0x0a,
pacch = 0x0b,
cbch52 = 0x0c,
pdtch = 0x0d,
ptcch = 0x0e,
cbch51 = 0x0f,
voice_f = 0x10,
voice_h = 0x11)
# GSMTAP_SIM_*
gsmtap_subtype_sim_construct = Enum(Int8ub,
apdu = 0x00,
atr = 0x01,
pps_req = 0x02,
pps_rsp = 0x03,
tpdu_hdr = 0x04,
tpdu_cmd = 0x05,
tpdu_rsp = 0x06,
tpdu_sw = 0x07)
gsmtap_subtype_tetra_construct = Enum(Int8ub,
bsch = 0x01,
aach = 0x02,
sch_hu = 0x03,
sch_hd = 0x04,
sch_f = 0x05,
bnch = 0x06,
stch = 0x07,
tch_f = 0x08,
dmo_sch_s = 0x09,
dmo_sch_h = 0x0a,
dmo_sch_f = 0x0b,
dmo_stch = 0x0c,
dmo_tch = 0x0d)
gsmtap_subtype_gmr1_construct = Enum(Int8ub,
unknown = 0x00,
bcch = 0x01,
ccch = 0x02,
pch = 0x03,
agch = 0x04,
bach = 0x05,
rach = 0x06,
cbch = 0x07,
sdcch = 0x08,
tachh = 0x09,
gbch = 0x0a,
tch3 = 0x10,
tch6 = 0x14,
tch9 = 0x18)
gsmtap_subtype_e1t1_construct = Enum(Int8ub,
lapd = 0x01,
fr = 0x02,
raw = 0x03,
trau16 = 0x04,
trau8 = 0x05)
gsmtap_arfcn_construct = BitStruct('pcs'/Flag, 'uplink'/Flag, 'arfcn'/BitsInteger(14))
gsmtap_hdr_construct = Struct('version'/Int8ub,
'hdr_len'/Int8ub,
'type'/gsmtap_type_construct,
'timeslot'/Int8ub,
'arfcn'/gsmtap_arfcn_construct,
'signal_dbm'/Int8sb,
'snr_db'/Int8sb,
'frame_nr'/Int32ub,
'sub_type'/Switch(this.type, {
'gsm_um': gsmtap_subtype_um_construct,
'gsm_um_burst': gsmtap_subtype_burst_construct,
'sim': gsmtap_subtype_sim_construct,
'tetra_i1': gsmtap_subtype_tetra_construct,
'tetra_i1_burst': gsmtap_subtype_tetra_construct,
'wimax_burst': gsmtap_subtype_wimax_burst_construct,
'gmr1_um': gsmtap_subtype_gmr1_construct,
'e1_t1': gsmtap_subtype_e1t1_construct,
}),
'antenna_nr'/Int8ub,
'sub_slot'/Int8ub,
'res'/Int8ub,
'body'/GreedyBytes)
osmocore_log_ts_construct = Struct('sec'/Int32ub, 'usec'/Int32ub)
osmocore_log_level_construct = Enum(Int8ub, debug=1, info=3, notice=5, error=7, fatal=8)
gsmtap_osmocore_log_hdr_construct = Struct('ts'/osmocore_log_ts_construct,
'proc_name'/PaddedString(16, 'ascii'),
'pid'/Int32ub,
'level'/osmocore_log_level_construct,
Bytes(3),
'subsys'/PaddedString(16, 'ascii'),
'src_file'/Struct('name'/PaddedString(32, 'ascii'), 'line_nr'/Int32ub))
class GsmtapMessage:
"""Class whose objects represent a single GSMTAP message. Can encode and decode messages."""
def __init__(self, encoded = None):
self.encoded = encoded
self.decoded = None
def decode(self):
self.decoded = parse_construct(gsmtap_hdr_construct, self.encoded)
return self.decoded
def encode(self, decoded):
self.encoded = gsmtap_hdr_construct.build(decoded)
return self.encoded
class GsmtapSource:
def __init__(self, bind_ip:str='127.0.0.1', bind_port:int=4729):
self.bind_ip = bind_ip
self.bind_port = bind_port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind((self.bind_ip, self.bind_port))
def read_packet(self) -> GsmtapMessage:
data, addr = self.sock.recvfrom(1024)
gsmtap_msg = GsmtapMessage(data)
gsmtap_msg.decode()
if gsmtap_msg.decoded['version'] != 0x02:
raise ValueError('Unknown GSMTAP version 0x%02x' % gsmtap_msg.decoded['version'])
return gsmtap_msg.decoded, addr

View File

@@ -17,9 +17,11 @@ 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 GreedyBytes, GreedyString
from osmocom.tlv import *
from osmocom.construct import *
from construct import *
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
# Table 91 + Section 8.2.1.2
class ApplicationId(BER_TLV_IE, tag=0x4f):

View File

@@ -1,19 +0,0 @@
# JavaCard related utilities
import zipfile
import struct
import sys
import io
def ijc_to_cap(in_file: io.IOBase, out_zip: zipfile.ZipFile, p : str = "foo"):
"""Convert an ICJ (Interoperable Java Card) file [back] to a CAP file."""
TAGS = ["Header", "Directory", "Applet", "Import", "ConstantPool", "Class", "Method", "StaticField", "RefLocation", "Export", "Descriptor", "Debug"]
b = in_file.read()
while len(b):
tag, size = struct.unpack('!BH', b[0:3])
out_zip.writestr(p+"/javacard/"+TAGS[tag-1]+".cap", b[0:3+size])
b = b[3+size:]
# example usage:
# with io.open(sys.argv[1],"rb") as f, zipfile.ZipFile(sys.argv[2], "wb") as z:
# ijc_to_cap(f, z)

View File

@@ -1,3 +1,8 @@
# 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,
@@ -5,8 +10,6 @@ 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,134 +0,0 @@
# -*- 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'
}

View File

@@ -1,43 +0,0 @@
# -*- 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'
}

View File

@@ -1,257 +0,0 @@
# -*- 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']],
}

View File

@@ -1,413 +0,0 @@
# -*- 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 typing import Optional, Tuple
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
from osmocom.utils import swap_nibbles, h2b, b2h
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.profile.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
def dec_msisdn(ef_msisdn: Hexstr) -> Optional[Tuple[int, int, Optional[str]]]:
"""
Decode MSISDN from EF.MSISDN or EF.ADN (same structure).
See 3GPP TS 31.102, section 4.2.26 and 4.4.2.3.
"""
# Convert from str to (kind of) 'bytes'
ef_msisdn = h2b(ef_msisdn)
# Make sure mandatory fields are present
if len(ef_msisdn) < 14:
raise ValueError("EF.MSISDN is too short")
# Skip optional Alpha Identifier
xlen = len(ef_msisdn) - 14
msisdn_lhv = ef_msisdn[xlen:]
# Parse the length (in bytes) of the BCD encoded number
bcd_len = msisdn_lhv[0]
# BCD length = length of dial num (max. 10 bytes) + 1 byte ToN and NPI
if bcd_len == 0xff:
return None
elif bcd_len > 11 or bcd_len < 1:
raise ValueError(
"Length of MSISDN (%d bytes) is out of range" % bcd_len)
# Parse ToN / NPI
ton = (msisdn_lhv[1] >> 4) & 0x07
npi = msisdn_lhv[1] & 0x0f
bcd_len -= 1
# No MSISDN?
if not bcd_len:
return (npi, ton, None)
msisdn = swap_nibbles(b2h(msisdn_lhv[2:][:bcd_len])).rstrip('f')
# International number 10.5.118/3GPP TS 24.008
if ton == 0x01:
msisdn = '+' + msisdn
return (npi, ton, msisdn)
def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
"""
Encode MSISDN as LHV so it can be stored to EF.MSISDN.
See 3GPP TS 31.102, section 4.2.26 and 4.4.2.3. (The result
will not contain the optional Alpha Identifier at the beginning.)
Default NPI / ToN values:
- NPI: ISDN / telephony numbering plan (E.164 / E.163),
- ToN: network specific or international number (if starts with '+').
"""
# If no MSISDN is supplied then encode the file contents as all "ff"
if msisdn in ["", "+"]:
return "ff" * 14
# Leading '+' indicates International Number
if msisdn[0] == '+':
msisdn = msisdn[1:]
ton = 0x01
# An MSISDN must not exceed 20 digits
if len(msisdn) > 20:
raise ValueError("msisdn must not be longer than 20 digits")
# Append 'f' padding if number of digits is odd
if len(msisdn) % 2 > 0:
msisdn += 'f'
# BCD length also includes NPI/ToN header
bcd_len = len(msisdn) // 2 + 1
npi_ton = (npi & 0x0f) | ((ton & 0x07) << 4) | 0x80
bcd = rpad(swap_nibbles(msisdn), 10 * 2) # pad to 10 octets
return ('%02x' % bcd_len) + ('%02x' % npi_ton) + bcd + ("ff" * 2)

View File

@@ -1,6 +1,6 @@
"""Code related to SIM/UICC OTA according to TS 102 225 + TS 31.115."""
# (C) 2021-2024 by Harald Welte <laforge@osmocom.org>
# (C) 2021-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
@@ -15,16 +15,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 pySim.construct import *
from pySim.utils import b2h
from pySim.sms import UserDataHeader
from construct import *
from bidict import bidict
import zlib
import abc
import struct
from typing import Optional, Tuple
from construct import Enum, Int8ub, Int16ub, Struct, Bytes, GreedyBytes, BitsInteger, BitStruct
from construct import Flag, Padding, Switch, this, PrefixedArray, GreedyRange
from osmocom.construct import *
from osmocom.utils import b2h
from pySim.sms import UserDataHeader
from typing import Optional
# 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
@@ -43,6 +42,23 @@ from pySim.sms import UserDataHeader
# KVN 1..f; KI1=KIc, KI2=KID, KI3=DEK
# TS 102 225 Table 5
ota_status_codes = bidict({
0x00: 'PoR OK',
0x01: 'RC/CC/DS failed',
0x02: 'CNTR low',
0x03: 'CNTR high',
0x04: 'CNTR blocked',
0x05: 'Ciphering error',
0x06: 'Unidentified security error',
0x07: 'Insufficient memory',
0x08: 'more time',
0x09: 'TAR unknown',
0x0a: 'Insufficient security level',
0x0b: 'Actual Response in SMS-SUBMIT', # 31.115
0x0c: 'Actual Response in USSD', # 31.115
})
# ETSI TS 102 225 Table 5 + 3GPP TS 31.115 Section 7
ResponseStatus = Enum(Int8ub, por_ok=0, rc_cc_ds_failed=1, cntr_low=2, cntr_high=3,
cntr_blocked=4, ciphering_error=5, undefined_security_error=6,
@@ -99,17 +115,6 @@ SmsCommandPacket = Struct('cmd_pkt_len'/Int16ub,
'tar'/Bytes(3),
'secured_data'/GreedyBytes)
# TS 102 226 Section 8.2.1.3.2.1
SimFileAccessAndToolkitAppSpecParams = Struct('access_domain'/Prefixed(Int8ub, GreedyBytes),
'prio_level_of_tk_app_inst'/Int8ub,
'max_num_of_timers'/Int8ub,
'max_text_length_for_menu_entry'/Int8ub,
'menu_entries'/PrefixedArray(Int8ub, Struct('id'/Int8ub,
'pos'/Int8ub)),
'max_num_of_channels'/Int8ub,
'msl'/Prefixed(Int8ub, GreedyBytes),
'tar_values'/Prefixed(Int8ub, GreedyRange(Bytes(3))))
class OtaKeyset:
"""The OTA related data (key material, counter) to be used in encrypt/decrypt."""
def __init__(self, algo_crypt: str, kic_idx: int, kic: bytes,
@@ -125,12 +130,12 @@ class OtaKeyset:
@property
def auth(self):
"""Return an instance of the matching OtaAlgoAuth."""
return OtaAlgoAuth.from_keyset(self)
return OtaAlgoAuth.fromKeyset(self)
@property
def crypt(self):
"""Return an instance of the matching OtaAlgoCrypt."""
return OtaAlgoCrypt.from_keyset(self)
return OtaAlgoCrypt.fromKeyset(self)
class OtaCheckError(Exception):
pass
@@ -141,31 +146,33 @@ class OtaDialect(abc.ABC):
def _compute_sig_len(self, spi:SPI):
if spi['rc_cc_ds'] == 'no_rc_cc_ds':
return 0
if spi['rc_cc_ds'] == 'rc': # CRC-32
elif spi['rc_cc_ds'] == 'rc': # CRC-32
return 4
if spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
elif 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
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
else:
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
@abc.abstractmethod
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
def encode_cmd(self, otak: OtaKeyset, tar: bytes, apdu: bytes) -> bytes:
pass
@abc.abstractmethod
def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
def decode_resp(self, otak: OtaKeyset, 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
from Cryptodome.Hash import CMAC
from Crypto.Cipher import DES, DES3, AES
from Crypto.Hash import CMAC
class OtaAlgo(abc.ABC):
iv = property(lambda self: bytes([0] * self.blocksize))
iv = b'\x00\x00\x00\x00\x00\x00\x00\x00'
blocksize = None
enum_name = None
@@ -201,7 +208,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(padded_data)
return self._encrypt(data)
def decrypt(self, data:bytes) -> bytes:
"""Decrypt given input bytes using the key material given in constructor."""
@@ -210,13 +217,15 @@ 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 from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoCrypt':
def fromKeyset(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:
@@ -248,7 +257,7 @@ class OtaAlgoAuth(OtaAlgo, abc.ABC):
pass
@classmethod
def from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoAuth':
def fromKeyset(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:
@@ -314,7 +323,7 @@ class OtaAlgoCryptAES(OtaAlgoCrypt):
class OtaAlgoAuthAES(OtaAlgoAuth):
name = 'AES'
enum_name = 'aes_cmac'
blocksize = 1 # AES CMAC doesn't need any padding by us
blocksize = 16 # TODO: is this needed?
def _sign(self, data:bytes) -> bytes:
cmac = CMAC.new(self.otak.kid, ciphermod=AES, mac_len=8)
cmac.update(data)
@@ -333,7 +342,6 @@ class OtaDialectSms(OtaDialect):
'response_status'/ResponseStatus,
'cc_rc'/Bytes(this.rhl-10),
'secured_data'/GreedyBytes)
hdr_construct = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
# length of signature in octets
@@ -344,7 +352,6 @@ class OtaDialectSms(OtaDialect):
len_cipher = 6 + len_sig + len(apdu)
padding = otak.crypt._get_padding(len_cipher, otak.crypt.blocksize)
pad_cnt = len(padding)
apdu = bytes(apdu) # make a copy so we don't modify the input data
apdu += padding
kic = {'key': otak.kic_idx, 'algo': otak.algo_crypt}
@@ -355,7 +362,8 @@ class OtaDialectSms(OtaDialect):
chl = 13 + len_sig
# CHL + SPI (+ KIC + KID)
part_head = self.hdr_construct.build({'chl': chl, 'spi':spi, 'kic':kic, 'kid':kid, 'tar':tar})
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))
# CNTR + PCNTR (CNTR not used)
@@ -397,54 +405,8 @@ class OtaDialectSms(OtaDialect):
#print("envelope_data: %s" % b2h(envelope_data))
if len(envelope_data) > 140:
raise ValueError('Cannot encode command in a single SMS; Fragmentation not implemented')
return envelope_data
def decode_cmd(self, otak: OtaKeyset, encoded: bytes) -> Tuple[bytes, dict, bytes]:
"""Decode an encoded (encrypted, signed) OTA SMS Command-APDU."""
if True: # TODO: how to decide?
cpl = int.from_bytes(encoded[:2], 'big')
part_head = encoded[2:2+8]
ciph = encoded[2+8:]
envelope_data = otak.crypt.decrypt(ciph)
else:
part_head = encoded[:8]
envelope_data = encoded[8:]
hdr_dec = self.hdr_construct.parse(part_head)
# strip counter part from front of envelope_data
part_cnt = envelope_data[:6]
cntr = int.from_bytes(part_cnt[:5], 'big')
pad_cnt = int.from_bytes(part_cnt[5:], 'big')
envelope_data = envelope_data[6:]
spi = hdr_dec['spi']
if spi['rc_cc_ds'] == 'cc':
# split cc from front of APDU
cc = envelope_data[:8]
apdu = envelope_data[8:]
# verify CC
temp_data = cpl.to_bytes(2, 'big') + part_head + part_cnt + apdu
otak.auth.check_sig(temp_data, cc)
elif spi['rc_cc_ds'] == 'rc':
# CRC32
crc32_rx = int.from_bytes(envelope_data[:4], 'big')
# FIXME: crc32_computed = zlip.crc32(
# FIXME: verify RC
raise NotImplementedError
apdu = envelope_data[4:]
elif spi['rc_cc_ds'] == 'no_rc_cc_ds':
apdu = envelope_data
else:
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
apdu = apdu[:len(apdu)-pad_cnt]
return hdr_dec['tar'], spi, apdu
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes) -> ("OtaDialectSms.SmsResponsePacket", Optional["CompactRemoteResp"]):
if isinstance(data, str):
data = h2b(data)
@@ -455,7 +417,7 @@ class OtaDialectSms(OtaDialect):
# POR with CC+CIPH: 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
if data[0] != 0x02:
raise ValueError('Unexpected UDL=0x%02x' % data[0])
udhd, remainder = UserDataHeader.from_bytes(data)
udhd, remainder = UserDataHeader.fromBytes(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
@@ -486,13 +448,13 @@ class OtaDialectSms(OtaDialect):
rpl_rhl_tar_cntr_pcntr_sts = remainder[:13]
# remove the CC/RC bytes
temp_data = udh + rpl_rhl_tar_cntr_pcntr_sts + remainder[13+len_sig:]
otak.auth.check_sig(temp_data, res['cc_rc'])
cc = otak.auth.check_sig(temp_data, res['cc_rc'])
# TODO: CRC
else:
raise OtaCheckError('Unknown por_rc_cc_ds: %s' % spi['por_rc_cc_ds'])
# TODO: ExpandedRemoteResponse according to TS 102 226 5.2.2
if res.response_status == 'por_ok' and len(res['secured_data']):
if res.response_status == 'por_ok':
dec = CompactRemoteResp.parse(res['secured_data'])
else:
dec = None

View File

@@ -1,77 +0,0 @@
import pprint
from pprint import PrettyPrinter
from functools import singledispatch, wraps
from typing import get_type_hints
from pySim.utils import b2h
def common_container_checks(f):
type_ = get_type_hints(f)['object']
base_impl = type_.__repr__
empty_repr = repr(type_()) # {}, [], ()
too_deep_repr = f'{empty_repr[0]}...{empty_repr[-1]}' # {...}, [...], (...)
@wraps(f)
def wrapper(object, context, maxlevels, level):
if type(object).__repr__ is not base_impl: # subclassed repr
return repr(object)
if not object: # empty, short-circuit
return empty_repr
if maxlevels and level >= maxlevels: # exceeding the max depth
return too_deep_repr
oid = id(object)
if oid in context: # self-reference
return pprint._recursion(object)
context[oid] = 1
result = f(object, context, maxlevels, level)
del context[oid]
return result
return wrapper
@singledispatch
def saferepr(object, context, maxlevels, level):
return repr(object)
@saferepr.register
def _handle_bytes(object: bytes, *args):
if len(object) <= 40:
return '"%s"' % b2h(object)
else:
return '"%s...%s"' % (b2h(object[:20]), b2h(object[-20:]))
@saferepr.register
@common_container_checks
def _handle_dict(object: dict, context, maxlevels, level):
level += 1
contents = [
f'{saferepr(k, context, maxlevels, level)}: '
f'{saferepr(v, context, maxlevels, level)}'
for k, v in sorted(object.items(), key=pprint._safe_tuple)
]
return f'{{{", ".join(contents)}}}'
@saferepr.register
@common_container_checks
def _handle_list(object: list, context, maxlevels, level):
level += 1
contents = [
f'{saferepr(v, context, maxlevels, level)}'
for v in object
]
return f'[{", ".join(contents)}]'
@saferepr.register
@common_container_checks
def _handle_tuple(object: tuple, context, maxlevels, level):
level += 1
if len(object) == 1:
return f'({saferepr(object[0], context, maxlevels, level)},)'
contents = [
f'{saferepr(v, context, maxlevels, level)}'
for v in object
]
return f'({", ".join(contents)})'
class HexBytesPrettyPrinter(PrettyPrinter):
def format(self, *args):
# it doesn't matter what the boolean values are here
return saferepr(*args), True, False

View File

@@ -21,14 +21,46 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import abc
import operator
from typing import List
from pySim.exceptions import SwMatchError
from pySim.commands import SimCardCommands
from pySim.filesystem import CardApplication, interpret_sw
from pySim.utils import all_subclasses
import abc
import operator
def _mf_select_test(scc: SimCardCommands, cla_byte: str, sel_ctrl: str) -> bool:
cla_byte_bak = scc.cla_byte
sel_ctrl_bak = scc.sel_ctrl
scc.reset_card()
scc.cla_byte = cla_byte
scc.sel_ctrl = sel_ctrl
rc = True
try:
scc.select_file('3f00')
except:
rc = False
scc.reset_card()
scc.cla_byte = cla_byte_bak
scc.sel_ctrl = sel_ctrl_bak
return rc
def match_uicc(scc: SimCardCommands) -> bool:
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
card is considered a UICC card.
"""
return _mf_select_test(scc, "00", "0004")
def match_sim(scc: SimCardCommands) -> bool:
""" Try to access MF via 2G APDUs (3GPP TS 11.11), if this works, the card
is also a simcard. This will be the case for most UICC cards, but there may
also be plain UICC cards without 2G support as well.
"""
return _mf_select_test(scc, "a0", "0000")
class CardProfile:
"""A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
@@ -45,7 +77,6 @@ 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)
@@ -55,8 +86,6 @@ 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
@@ -95,39 +124,13 @@ class CardProfile:
return data_hex
@staticmethod
def _mf_select_test(scc: SimCardCommands,
cla_byte: str, sel_ctrl: str,
fids: List[str]) -> bool:
"""Helper function used by some derived _try_match_card() methods."""
scc.reset_card()
scc.cla_byte = cla_byte
scc.sel_ctrl = sel_ctrl
for fid in fids:
scc.select_file(fid)
@classmethod
@abc.abstractmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
"""Try to see if the specific profile matches the card. This method is a
def match_with_card(scc: SimCardCommands) -> bool:
"""Check if the specific profile matches the card. This method is a
placeholder that is overloaded by specific dirived classes. The method
actively probes the card to make sure the profile class matches the
physical card. This usually also means that the card is reset during
the process, so this method must not be called at random times. It may
only be called on startup. If there is no exception raised, we assume
the card matches the profile.
Args:
scc: SimCardCommands class
"""
pass
@classmethod
def match_with_card(cls, scc: SimCardCommands) -> bool:
"""Check if the specific profile matches the card. The method
actively probes the card to make sure the profile class matches the
physical card. This usually also means that the card is reset during
the process, so this method must not be called at random times. It may
only be called on startup.
Args:
@@ -135,17 +138,7 @@ class CardProfile:
Returns:
match = True, no match = False
"""
sel_backup = scc.sel_ctrl
cla_backup = scc.cla_byte
try:
cls._try_match_card(scc)
return True
except SwMatchError:
return False
finally:
scc.sel_ctrl = sel_backup
scc.cla_byte = cla_backup
scc.reset_card()
return False
@staticmethod
def pick(scc: SimCardCommands):
@@ -157,32 +150,3 @@ 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."""

View File

@@ -1,208 +0,0 @@
# coding=utf-8
"""R-UIM (Removable User Identity Module) card profile (see 3GPP2 C.S0023-D)
(C) 2023 by Vadim Yanitskiy <fixeria@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 enum
from construct import Bytewise, BitStruct, BitsInteger, Struct, FlagsEnum
from osmocom.utils import *
from osmocom.construct import *
from pySim.filesystem import *
from pySim.profile import CardProfile, CardProfileAddon
from pySim.profile.ts_51_011 import CardProfileSIM
from pySim.profile.ts_51_011 import DF_TELECOM, DF_GSM
from pySim.profile.ts_51_011 import EF_ServiceTable
# Mapping between CDMA Service Number and its description
EF_CST_map = {
1 : 'CHV disable function',
2 : 'Abbreviated Dialing Numbers (ADN)',
3 : 'Fixed Dialing Numbers (FDN)',
4 : 'Short Message Storage (SMS)',
5 : 'HRPD',
6 : 'Enhanced Phone Book',
7 : 'Multi Media Domain (MMD)',
8 : 'SF_EUIMID-based EUIMID',
9 : 'MEID Support',
10 : 'Extension1',
11 : 'Extension2',
12 : 'SMS Parameters',
13 : 'Last Number Dialled (LND)',
14 : 'Service Category Program for BC-SMS',
15 : 'Messaging and 3GPD Extensions',
16 : 'Root Certificates',
17 : 'CDMA Home Service Provider Name',
18 : 'Service Dialing Numbers (SDN)',
19 : 'Extension3',
20 : '3GPD-SIP',
21 : 'WAP Browser',
22 : 'Java',
23 : 'Reserved for CDG',
24 : 'Reserved for CDG',
25 : 'Data Download via SMS Broadcast',
26 : 'Data Download via SMS-PP',
27 : 'Menu Selection',
28 : 'Call Control',
29 : 'Proactive R-UIM',
30 : 'AKA',
31 : 'IPv6',
32 : 'RFU',
33 : 'RFU',
34 : 'RFU',
35 : 'RFU',
36 : 'RFU',
37 : 'RFU',
38 : '3GPD-MIP',
39 : 'BCMCS',
40 : 'Multimedia Messaging Service (MMS)',
41 : 'Extension 8',
42 : 'MMS User Connectivity Parameters',
43 : 'Application Authentication',
44 : 'Group Identifier Level 1',
45 : 'Group Identifier Level 2',
46 : 'De-Personalization Control Keys',
47 : 'Cooperative Network List',
}
######################################################################
# DF.CDMA
######################################################################
class EF_SPN(TransparentEF):
'''3.4.31 CDMA Home Service Provider Name'''
_test_de_encode = [
( "010801536b796c696e6b204e57ffffffffffffffffffffffffffffffffffffffffffff",
{ 'rfu1' : 0, 'show_in_hsa' : True, 'rfu2' : 0,
'char_encoding' : 8, 'lang_ind' : 1, 'spn' : 'Skylink NW' } ),
]
def __init__(self, fid='6f41', sfid=None, name='EF.SPN',
desc='Service Provider Name', size=(35, 35), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = BitStruct(
# Byte 1: Display Condition
'rfu1'/BitsRFU(7),
'show_in_hsa'/Flag,
# Byte 2: Character Encoding
'rfu2'/BitsRFU(3),
'char_encoding'/BitsInteger(5), # see C.R1001-G
# Byte 3: Language Indicator
'lang_ind'/BitsInteger(8), # see C.R1001-G
# Bytes 4-35: Service Provider Name
'spn'/Bytewise(GsmString(32))
)
class EF_AD(TransparentEF):
'''3.4.33 Administrative Data'''
_test_de_encode = [
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : '' } ),
]
_test_no_pad = True
class OP_MODE(enum.IntEnum):
normal = 0x00
type_approval = 0x80
normal_and_specific_facilities = 0x01
type_approval_and_specific_facilities = 0x81
maintenance_off_line = 0x02
cell_test = 0x04
def __init__(self, fid='6f43', sfid=None, name='EF.AD',
desc='Service Provider Name', size=(3, None), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = Struct(
# Byte 1: Display Condition
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
# Bytes 2-3: Additional information
'additional_info'/HexAdapter(Bytes(2)),
# Bytes 4..: RFU
'rfu'/HexAdapter(GreedyBytesRFU),
)
class EF_SMS(LinFixedEF):
'''3.4.27 Short Messages'''
def __init__(self, fid='6f3c', sfid=None, name='EF.SMS', desc='Short messages', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(2, 255), **kwargs)
self._construct = Struct(
# Byte 1: Status
'status'/BitStruct(
'rfu87'/BitsRFU(2),
'protection'/Flag,
'rfu54'/BitsRFU(2),
'status'/FlagsEnum(BitsInteger(2), read=0, to_be_read=1, sent=2, to_be_sent=3),
'used'/Flag,
),
# Byte 2: Length
'length'/Int8ub,
# Bytes 3..: SMS Transport Layer Message
'tpdu'/Bytes(lambda ctx: ctx.length if ctx.status.used else 0),
)
class DF_CDMA(CardDF):
def __init__(self):
super().__init__(fid='7f25', name='DF.CDMA',
desc='CDMA related files (3GPP2 C.S0023-D)')
files = [
# TODO: lots of other files
EF_ServiceTable('6f32', None, 'EF.CST',
'CDMA Service Table', table=EF_CST_map, size=(5, 16)),
EF_SPN(),
EF_AD(),
EF_SMS(),
]
self.add_files(files)
class CardProfileRUIM(CardProfile):
'''R-UIM card profile as per 3GPP2 C.S0023-D'''
ORDER = 20
def __init__(self):
super().__init__('R-UIM', desc='CDMA R-UIM Card', cla="a0",
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
@staticmethod
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(data_hex)
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
""" Try to access MF/DF.CDMA via 2G APDUs (3GPP TS 11.11), if this works,
the card is considered an R-UIM card for CDMA."""
cls._mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
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)

View File

@@ -1,58 +0,0 @@
# 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/>.
from pySim.profile.ts_102_221 import CardProfileUICC
from pySim.commands import SimCardCommands
from pySim.euicc import CardApplicationISDR, AID_ISD_R, AID_ECASD, GetCertsReq, GetCertsResp
class CardProfileEuiccSGP32(CardProfileUICC):
ORDER = 5
def __init__(self):
super().__init__(name='IoT eUICC (SGP.32)')
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
# try a command only supported by SGP.32
scc.cla_byte = "00"
scc.select_adf(AID_ISD_R)
CardApplicationISDR.store_data_tlv(scc, GetCertsReq(), GetCertsResp)
class CardProfileEuiccSGP22(CardProfileUICC):
ORDER = 6
def __init__(self):
super().__init__(name='Consumer eUICC (SGP.22)')
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
# try to read EID from ISD-R
scc.cla_byte = "00"
scc.select_adf(AID_ISD_R)
eid = CardApplicationISDR.get_eid(scc)
# TODO: Store EID identity?
class CardProfileEuiccSGP02(CardProfileUICC):
ORDER = 7
def __init__(self):
super().__init__(name='M2M eUICC (SGP.02)')
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
scc.cla_byte = "00"
scc.select_adf(AID_ECASD)
scc.get_data(0x5a)
# TODO: Store EID identity?

View File

@@ -1,394 +0,0 @@
# coding=utf-8
"""Card Profile of ETSI TS 102 221, the core UICC spec.
(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
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 Struct, FlagsEnum, GreedyString
from osmocom.construct import *
from osmocom.utils import *
from osmocom.tlv import BER_TLV_IE
from pySim.utils import *
from pySim.filesystem import *
from pySim.profile import CardProfile
from pySim import iso7816_4
from pySim.ts_102_221 import decode_select_response, ts_102_22x_cmdset
from pySim.ts_102_221 import AM_DO_EF, SC_DO, AdditionalInterfacesSupport, AdditionalTermCapEuicc
from pySim.ts_102_221 import TerminalPowerSupply, ExtendedLchanTerminalSupport, TerminalCapability
# 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.profile.ts_51_011 import AddonSIM, EF_ICCID, EF_PL
from pySim.profile.gsm_r import AddonGSMR
from pySim.profile.cdma_ruim import AddonRUIM
# TS 102 221 Section 13.1
class EF_DIR(LinFixedEF):
_test_de_encode = [
( '61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150',
{ "application_template": [ { "application_id": h2b("a0000000871002ffffffff8907090000") },
{ "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
_construct = GreedyString('ascii')
# see https://github.com/PyCQA/pylint/issues/5794
#pylint: disable=undefined-variable
class ApplicationTemplate(BER_TLV_IE, tag=0x61,
nested=[iso7816_4.ApplicationId, ApplicationLabel, iso7816_4.FileReference,
iso7816_4.CommandApdu, iso7816_4.DiscretionaryData,
iso7816_4.DiscretionaryTemplate, iso7816_4.URL,
iso7816_4.ApplicationRelatedDOSet]):
pass
def __init__(self, fid='2f00', sfid=0x1e, name='EF.DIR', desc='Application Directory'):
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(5, 54))
self._tlv = EF_DIR.ApplicationTemplate
# TS 102 221 Section 13.4
class EF_ARR(LinFixedEF):
_test_de_encode = [
( '800101a40683010a950108800106900080016097008401d4a40683010a950108',
[ [ { "access_mode": [ "read_search_compare" ] },
{ "control_reference_template": "ADM1" } ],
[ { "access_mode": [ "write_append", "update_erase" ] },
{ "always": None } ],
[ { "access_mode": [ "delete_file", "terminate_ef" ] },
{ "never": None } ],
[ { "command_header": { "INS": 212 } },
{ "control_reference_template": "ADM1" } ]
] ),
( '80010190008001029700800118a40683010a9501088401d4a40683010a950108',
[ [ { "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" } ]
] ),
]
def __init__(self, fid='2f06', sfid=0x06, name='EF.ARR', desc='Access Rule Reference'):
super().__init__(fid, sfid=sfid, name=name, desc=desc)
# add those commands to the general commands of a TransparentEF
self.shell_commands += [self.AddlShellCommands()]
@staticmethod
def flatten(inp: list):
"""Flatten the somewhat deep/complex/nested data returned from decoder."""
def sc_abbreviate(sc):
if 'always' in sc:
return 'always'
elif 'never' in sc:
return 'never'
elif 'control_reference_template' in sc:
return sc['control_reference_template']
else:
return sc
by_mode = {}
for t in inp:
am = t[0]
sc = t[1]
sc_abbr = sc_abbreviate(sc)
if 'access_mode' in am:
for m in am['access_mode']:
by_mode[m] = sc_abbr
elif 'command_header' in am:
ins = am['command_header']['INS']
if 'CLA' in am['command_header']:
cla = am['command_header']['CLA']
else:
cla = None
cmd = ts_102_22x_cmdset.lookup(ins, cla)
if cmd:
name = cmd.name.lower().replace(' ', '_')
by_mode[name] = sc_abbr
else:
raise ValueError
else:
raise ValueError
return by_mode
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)
# we cannot pass the result through flatten() here, as we don't have a related
# 'un-flattening' decoder, and hence would be unable to encode :(
return dec[0]
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):
@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 = self._cmd.lchan.selected_file.flatten(data)
self._cmd.poutput_json(data, opts.oneline)
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
def do_read_arr_records(self, opts):
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
# 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 = self._cmd.lchan.selected_file.flatten(data)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
# TS 102 221 Section 13.6
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))
addl_info = FlagsEnum(Byte, req_inc_idle_current=1,
support_uicc_suspend=2)
self._construct = Struct(
'max_current_mA'/Int8ub, 't_op_s'/Int8ub, 'addl_info'/addl_info)
class CardProfileUICC(CardProfile):
ORDER = 10
def __init__(self, name='UICC'):
files = [
EF_DIR(),
EF_ICCID(),
EF_PL(),
EF_ARR(),
# FIXME: DF.CD
EF_UMPC(),
]
addons = [
AddonSIM,
AddonGSMR,
AddonRUIM,
]
sw = {
'Normal': {
'9000': 'Normal ending of the command',
'91xx': 'Normal ending of the command, with extra information from the proactive UICC containing a command for the terminal',
'92xx': 'Normal ending of the command, with extra information concerning an ongoing data transfer session',
},
'Postponed processing': {
'9300': 'SIM Application Toolkit is busy. Command cannot be executed at present, further normal commands are allowed',
},
'Warnings': {
'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/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',
'62f3': 'Response data available',
'63f1': 'More data expected',
'63f2': 'More data expected and proactive command pending',
'63cx': 'Command successful but after using an internal update retry routine X times',
},
'Execution errors': {
'6400': 'No information given, state of non-volatile memory unchanged',
'6500': 'No information given, state of non-volatile memory changed',
'6581': 'Memory problem',
},
'Checking errors': {
'6700': 'Wrong length',
'67xx': 'The interpretation of this status word is command dependent',
'6b00': 'Wrong parameter(s) P1-P2',
'6d00': 'Instruction code not supported or invalid',
'6e00': 'Class not supported',
'6f00': 'Technical problem, no precise diagnosis',
'6fxx': 'The interpretation of this status word is command dependent',
},
'Functions in CLA not supported': {
'6800': 'No information given',
'6881': 'Logical channel not supported',
'6882': 'Secure messaging not supported',
},
'Command not allowed': {
'6900': 'No information given',
'6981': 'Command incompatible with file structure',
'6982': 'Security status not satisfied',
'6983': 'Authentication/PIN method blocked',
'6984': 'Referenced data invalidated',
'6985': 'Conditions of use not satisfied',
'6986': 'Command not allowed (no EF selected)',
'6989': 'Command not allowed - secure channel - security not satisfied',
},
'Wrong parameters': {
'6a80': 'Incorrect parameters in the data field',
'6a81': 'Function not supported',
'6a82': 'File not found',
'6a83': 'Record not found',
'6a84': 'Not enough memory space',
'6a86': 'Incorrect parameters P1 to P2',
'6a87': 'Lc inconsistent with P1 to P2',
'6a88': 'Referenced data not found',
},
'Application errors': {
'9850': 'INCREASE cannot be performed, max value reached',
'9862': 'Authentication error, application specific',
'9863': 'Security session or association expired',
'9864': 'Minimum UICC suspension time is too long',
},
}
super().__init__(name, desc='ETSI TS 102 221', cla="00",
sel_ctrl="0004", files_in_mf=files, sw=sw,
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
@staticmethod
def decode_select_response(data_hex: str) -> object:
"""ETSI TS 102 221 Section 11.1.1.3"""
return decode_select_response(data_hex)
@classmethod
def _try_match_card(cls, scc: SimCardCommands) -> None:
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
card is considered a UICC card."""
cls._mf_select_test(scc, "00", "0004", ["3f00"])
@with_default_category('TS 102 221 Specific Commands')
class AddlShellCommands(CommandSet):
suspend_uicc_parser = argparse.ArgumentParser()
suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
help='Proposed minimum duration of suspension')
suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
help='Proposed maximum duration of suspension')
# not ISO7816-4 but TS 102 221
@cmd2.with_argparser(suspend_uicc_parser)
def do_suspend_uicc(self, opts):
"""Perform the SUSPEND UICC command. Only supported on some UICC (check EF.UMPC)."""
(duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
max_len_secs=opts.max_duration_secs)
self._cmd.poutput(
'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
resume_uicc_parser = argparse.ArgumentParser()
resume_uicc_parser.add_argument('TOKEN', type=str, help='Token provided during SUSPEND')
@cmd2.with_argparser(resume_uicc_parser)
def do_resume_uicc(self, opts):
"""Perform the REUSME UICC operation. Only supported on some UICC. Also: A power-cycle
of the card is required between SUSPEND and RESUME, and only very few non-RESUME
commands are permitted between SUSPEND and RESUME. See TS 102 221 Section 11.1.22."""
self._cmd.card._scc.resume_uicc(opts.TOKEN)
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)

View File

@@ -1,628 +0,0 @@
# 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 osmocom.utils import h2b, i2h, is_hex, Hexstr
from osmocom.tlv import bertlv_parse_one
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()):
self.lchan[lchan_nr].scc.scp = None
if lchan_nr == 0:
continue
del self.lchan[lchan_nr]
atr = i2h(self.card.reset())
if cmd_app:
cmd_app.lchan = self.lchan[0]
# 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 selected_file_record_len(self) -> Optional[int]:
return self.selected_file_fcp['file_descriptor'].get('record_len')
def selected_file_size(self) -> Optional[int]:
return self.selected_file_fcp.get('file_size')
def selected_file_reserved_file_size(self) -> Optional[int]:
return self.selected_file_fcp['proprietary_information'].get('reserved_file_size')
def selected_file_maximum_file_size(self) -> Optional[int]:
return self.selected_file_fcp['proprietary_information'].get('maximum_file_size')
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 get_file_by_name(self, name: str) -> CardFile:
"""Obtain the file object from the file system tree by its name without actually selecting the file.
Returns:
CardFile() instance or None"""
# 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'
else:
pathlist = [name]
# start in the current working directory (we can still
# select any ADF and the MF from here, so those will be
# among the selectables).
file = self.get_cwd()
for p in pathlist:
# Look for the next file in the path list
selectables = file.get_selectables()
file = None
for selectable in selectables:
if selectable == p:
file = selectables[selectable]
break
# When we hit none, then the given path must be invalid
if file is None:
return None
# Return the file object found at the tip of the path
return file
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)
# unregister commands of old file
self.unregister_cmds(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_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
self.register_cmds(cmd_app)
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)
"""
if not isinstance(file, CardADF) and self.selected_adf and self.selected_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 we will first select an arbitrary ADF that has file system support first
# and then continue normally.
for selectable in self.rs.mf.get_selectables().items():
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
self.select(selectable[1].name, cmd_app)
break
# 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))
# unregister commands of old file
self.unregister_cmds(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_filename(self, name: str):
"""Get the related CardFile object for a specified filename."""
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, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
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, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
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, self.selected_file_size())
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, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
# 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, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
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, self.selected_file_record_len())
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, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
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, but %s is %s" % (self.selected_file,
self.selected_file.__class__.__mro__))
return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
def register_cmds(self, cmd_app=None):
"""Register command set that is associated with the currently selected 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 unregister_cmds(self, cmd_app=None):
"""Unregister command set that is associated with the currently selected file"""
if cmd_app and self.selected_file.shell_commands:
for c in self.selected_file.shell_commands:
cmd_app.unregister_command_set(c)

View File

@@ -1,39 +0,0 @@
# 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 osmocom.utils import b2h, h2b, Hexstr
from pySim.utils import ResTuple
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

View File

@@ -19,14 +19,15 @@
import typing
import abc
from bidict import bidict
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger
from pprint import pprint as pp
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger, Flag
from construct import Struct, Enum, Tell, BitStruct, this, Padding
from construct import Prefixed, GreedyRange, GreedyBytes
from osmocom.construct import HexAdapter, BcdAdapter, TonNpi
from osmocom.utils import Hexstr, h2b, b2h
from smpp.pdu import pdu_types, operations
from pySim.construct import HexAdapter, BcdAdapter, TonNpi
from pySim.utils import Hexstr, h2b, b2h
from smpp.pdu import pdu_types
BytesOrHex = typing.Union[Hexstr, bytes]
@@ -50,34 +51,30 @@ class UserDataHeader:
return False
@classmethod
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
def fromBytes(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 to_bytes(self) -> bytes:
def toBytes(self) -> bytes:
return self._construct.build({'ies':self.ies, 'data':b''})
def smpp_dcs_is_8bit(dcs: pdu_types.DataCoding) -> bool:
"""Determine if the given SMPP data coding scheme is 8-bit or not."""
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
return True
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED_COMMON):
return True
# pySim/sms.py:72:21: E1101: Instance of 'DataCodingScheme' has no 'GSM_MESSAGE_CLASS' member (no-member)
# pylint: disable=no-member
if dcs.scheme == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS and dcs.schemeData['msgCoding'] == pdu_types.DataCodingGsmMsgCoding.DATA_8BIT:
return True
else:
return False
def ensure_smpp_is_8bit(dcs: pdu_types.DataCoding):
"""Assert if given SMPP data coding scheme is not 8-bit."""
if not smpp_dcs_is_8bit(dcs):
if not smpp_dcs_is_8bit(smpp_pdu.params['data_coding']):
raise ValueError('We only support 8bit coded SMS for now')
class AddressField:
@@ -86,26 +83,6 @@ class AddressField:
'type_of_addr'/TonNpi,
'digits'/BcdAdapter(Bytes(this.addr_len//2 + this.addr_len%2)),
'tell'/Tell)
smpp_map_npi = bidict({
'UNKNOWN': 'unknown',
'ISDN': 'isdn_e164',
'DATA': 'data_x121',
'TELEX': 'telex_f69',
'LAND_MOBILE': 'sc_specific6',
'NATIONAL': 'national',
'PRIVATE': 'private',
'ERMES': 'ermes',
})
smpp_map_ton = bidict({
'UNKNOWN': 'unknown',
'INTERNATIONAL': 'international',
'NATIONAL': 'national',
'NETWORK_SPECIFIC': 'network_specific',
'SUBSCRIBER_NUMBER': 'short_code',
'ALPHANUMERIC': 'alphanumeric',
'ABBREVIATED': 'abbreviated',
})
def __init__(self, digits, ton='unknown', npi='unknown'):
self.ton = ton
@@ -116,11 +93,12 @@ class AddressField:
return 'AddressField(TON=%s, NPI=%s, %s)' % (self.ton, self.npi, self.digits)
@classmethod
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
def fromBytes(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)
res = cls._construct.parse(inb)
#pp(res)
#print("size: %s" % cls._construct.sizeof())
ton = res['type_of_addr']['type_of_number']
npi = res['type_of_addr']['numbering_plan_id']
@@ -128,16 +106,32 @@ class AddressField:
return cls(res['digits'][:res['addr_len']], ton, npi), inb[res['tell']:]
@classmethod
def from_smpp(cls, addr, ton, npi) -> 'AddressField':
def fromSmpp(cls, addr, ton, npi) -> 'AddressField':
"""Construct an AddressField from {source,dest}_addr_{,ton,npi} attributes of smpp.pdu."""
smpp_map_npi = {
'UNKNOWN': 'unknown',
'ISDN': 'isdn_e164',
'DATA': 'data_x121',
'TELEX': 'telex_f69',
'LAND_MOBILE': 'sc_specific6',
'NATIONAL': 'national',
'PRIVATE': 'private',
'ERMES': 'ermes',
}
smpp_map_ton = {
'UNKNOWN': 'unknown',
'INTERNATIONAL': 'international',
'NATIONAL': 'national',
'NETWORK_SPECIFIC': 'network_specific',
'SUBSCRIBER_NUMBER': 'short_code',
'ALPHANUMERIC': 'alphanumeric',
'ABBREVIATED': 'abbreviated',
}
# return the resulting instance
return cls(addr.decode('ascii'), AddressField.smpp_map_ton[ton.name], AddressField.smpp_map_npi[npi.name])
return cls(addr.decode('ascii'), smpp_map_ton[ton.name], smpp_map_npi[npi.name])
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 to_bytes(self) -> bytes:
def toBytes(self) -> bytes:
"""Encode the AddressField into the binary representation as used in T-PDU."""
num_digits = len(self.digits)
if num_digits % 2:
@@ -168,7 +162,7 @@ class SMS_TPDU(abc.ABC):
class SMS_DELIVER(SMS_TPDU):
"""Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction."""
"""Representation of a SMS-DELIVER T-PDU."""
flags_construct = BitStruct('tp_rp'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag, 'tp_sri'/Flag,
Padding(1), 'tp_mms'/Flag, 'tp_mti'/BitsInteger(2))
def __init__(self, **kwargs):
@@ -184,12 +178,13 @@ 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 from_bytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
def fromBytes(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.from_bytes(inb[1:])
oa, remainder = AddressField.fromBytes(inb[1:])
d['tp_oa'] = oa
offset = 0
d['tp_pid'] = remainder[offset]
@@ -204,7 +199,7 @@ class SMS_DELIVER(SMS_TPDU):
d['tp_ud'] = remainder[offset:]
return cls(**d)
def to_bytes(self) -> bytes:
def toBytes(self) -> bytes:
"""Encode a SMS_DELIVER instance to the binary encoded format as used in T-PDU."""
outb = bytearray()
d = {
@@ -213,7 +208,7 @@ class SMS_DELIVER(SMS_TPDU):
}
flags = SMS_DELIVER.flags_construct.build(d)
outb.extend(flags)
outb.extend(self.tp_oa.to_bytes())
outb.extend(self.tp_oa.toBytes())
outb.append(self.tp_pid)
outb.append(self.tp_dcs)
outb.extend(self.tp_scts)
@@ -223,18 +218,18 @@ class SMS_DELIVER(SMS_TPDU):
return outb
@classmethod
def from_smpp(cls, smpp_pdu) -> 'SMS_DELIVER':
def fromSmpp(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.from_smpp_submit(smpp_pdu)
return cls.fromSmppSubmit(cls, smpp_pdu)
else:
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
@classmethod
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_DELIVER':
def fromSmppSubmit(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.from_smpp(smpp_pdu.params['source_addr'],
tp_oa = AddressField.fromSmpp(smpp_pdu.params['source_addr'],
smpp_pdu.params['source_addr_ton'],
smpp_pdu.params['source_addr_npi'])
tp_ud = smpp_pdu.params['short_message']
@@ -256,7 +251,7 @@ class SMS_DELIVER(SMS_TPDU):
class SMS_SUBMIT(SMS_TPDU):
"""Representation of a SMS-SUBMIT T-PDU. This is the MS/UE -> network (uplink) direction."""
"""Representation of a SMS-DELIVER T-PDU."""
flags_construct = BitStruct('tp_srr'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag,
'tp_vpf'/Enum(BitsInteger(2), none=0, relative=2, enhanced=1, absolute=3),
'tp_rd'/Flag, 'tp_mti'/BitsInteger(2))
@@ -274,7 +269,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 from_bytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
def fromBytes(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):
@@ -283,7 +278,7 @@ class SMS_SUBMIT(SMS_TPDU):
offset += 1
d['tp_mr']= inb[offset]
offset += 1
da, remainder = AddressField.from_bytes(inb[2:])
da, remainder = AddressField.fromBytes(inb[2:])
d['tp_da'] = da
offset = 0
@@ -301,10 +296,12 @@ 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]
@@ -312,7 +309,7 @@ class SMS_SUBMIT(SMS_TPDU):
d['tp_ud'] = remainder[offset:]
return cls(**d)
def to_bytes(self) -> bytes:
def toBytes(self) -> bytes:
"""Encode a SMS_SUBMIT instance to the binary encoded format as used in T-PDU."""
outb = bytearray()
d = {
@@ -322,7 +319,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.to_bytes())
outb.extend(self.tp_da.toBytes())
outb.append(self.tp_pid)
outb.append(self.tp_dcs)
if self.tp_vpf != 'none':
@@ -332,20 +329,20 @@ class SMS_SUBMIT(SMS_TPDU):
return outb
@classmethod
def from_smpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
"""Construct a SMS_SUBMIT instance from the format used by smpp.pdu."""
def fromSmpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
"""Construct a SMS_DELIVER instance from the format used by smpp.pdu."""
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
return cls.from_smpp_submit(smpp_pdu)
return cls.fromSmppSubmit(cls, smpp_pdu)
else:
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
@classmethod
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_SUBMIT':
"""Construct a SMS_SUBMIT instance from the submit format used by smpp.pdu."""
def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_SUBMIT':
"""Construct a SMS_DELIVER instance from the submit format used by smpp.pdu."""
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
tp_da = AddressField.from_smpp(smpp_pdu.params['destination_addr'],
smpp_pdu.params['dest_addr_ton'],
smpp_pdu.params['dest_addr_npi'])
tp_da = AddressField.fromSmpp(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:
@@ -366,8 +363,8 @@ class SMS_SUBMIT(SMS_TPDU):
}
return cls(**d)
def to_smpp(self) -> pdu_types.PDU:
"""Translate a SMS_SUBMIT instance to a smpp.pdu.operations.SubmitSM instance."""
def toSmpp(self) -> pdu_types.PDU:
"""Translate a SMS_DELIVER instance to a smpp.pdu.pdu_types.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)
if self.tp_rp:
@@ -378,21 +375,20 @@ 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.to_smpp()
return operations.SubmitSM(service_type='',
source_addr_ton=pdu_types.AddrTon.ALPHANUMERIC,
source_addr_npi=pdu_types.AddrNpi.UNKNOWN,
source_addr='simcard',
dest_addr_ton=ton,
dest_addr_npi=npi,
destination_addr=daddr,
esm_class=esm_class,
protocol_id=self.tp_pid,
priority_flag=pdu_types.PriorityFlag.LEVEL_0,
#schedule_delivery_time,
#validity_period,
registered_delivery=reg_del,
replace_if_present_flag=repl_if,
data_coding=dc,
#sm_default_msg_id,
short_message=self.tp_ud)
return pdu_types.SubmitSM(service_type='',
source_addr_ton=pdu_types.AddrTon.ALPHANUMERIC,
source_addr_npi=pdu_types.AddrNpi.UNKNOWN,
source_addr='simcard',
dest_addr_ton=FIXME(self.tp_da.ton),
dest_addr_npi=FIXME(self.tp_da.npi),
destination_addr=self.tp_da.digits,
esm_class=esm_class,
protocol_id=self.tp_pid,
priority_flag=pdu_types.PriorityFlag.LEVEL_0,
#schedule_delivery_time,
#validity_period,
registered_delivery=reg_del,
replace_if_present_flag=repl_if,
data_coding=dc,
#sm_default_msg_id,
short_message=self.tp_ud)

View File

@@ -1,7 +1,7 @@
# coding=utf-8
"""Utilities / Functions related to sysmocom SJA2/SJA5 cards
"""Utilities / Functions related to sysmocom SJA2 cards
(C) 2021-2023 by Harald Welte <laforge@osmocom.org>
(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
@@ -17,16 +17,13 @@ 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 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 osmocom.utils import *
from osmocom.construct import *
from pytlv.TLV import *
from struct import pack, unpack
from pySim.utils import *
from pySim.filesystem import *
from pySim.profile.ts_102_221 import CardProfileUICC
from pySim.runtime import RuntimeState
from pySim.ts_102_221 import CardProfileUICC
from pySim.construct import *
from construct import *
import pySim
key_type2str = {
@@ -48,73 +45,69 @@ mac_length = {
class EF_PIN(TransparentEF):
_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'):
def __init__(self, fid, name):
super().__init__(fid, name=name, desc='%s PIN file' % name)
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))
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
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)
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)))
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(),
}
class EF_0348_KEY(LinFixedEF):
def __init__(self, fid='6f22', name='EF.0348_KEY', desc='TS 03.48 OTA Keys'):
super().__init__(fid, name=name, desc=desc, rec_len=(27, 35))
KeyLenAndType = BitStruct('mac_length'/Mapping(Bit, {8:0, 4:1}),
'algorithm'/Enum(Bit, des=0, aes=1),
'key_length'/MultiplyAdapter(BitsInteger(3), 8),
'_rfu'/BitsRFU(1),
'key_type'/Enum(BitsInteger(2), kic=0, kid=1, kik=2, any=3))
self._construct = Struct('security_domain'/Int8ub,
'key_set_version'/Int8ub,
'key_len_and_type'/KeyLenAndType,
'key'/HexAdapter(Bytes(this.key_len_and_type.key_length)))
def _decode_record_bin(self, raw_bin_data):
u = unpack('!BBB', raw_bin_data[0:3])
key_algo = (u[2] >> 6) & 1
key_length = ((u[2] >> 3) & 3) * 8
return {'sec_domain': u[0],
'key_set_version': u[1],
'key_type': key_type2str[u[2] & 3],
'key_length': key_length,
'algorithm': key_algo2str[key_algo],
'mac_length': mac_length[(u[2] >> 7)],
'key': raw_bin_data[3:key_length].hex()
}
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))
self._construct = Struct('sec_domain'/Int8ub,
'key_set_version'/Int8ub,
'counter'/HexAdapter(Bytes(5)))
def _decode_record_bin(self, raw_bin_data):
u = unpack('!BB5s', raw_bin_data)
return {'sec_domain': u[0], 'key_set_version': u[1], 'counter': u[2]}
class EF_SIM_AUTH_COUNTER(TransparentEF):
@@ -124,40 +117,60 @@ 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))
self._construct = Struct('sec_domain'/Int8ub,
'key_set_version'/Int8ub,
'counter'/Int16ub,
'rfu'/Int8ub)
def _decode_record_bin(self, raw_bin_data):
u = unpack('!BBHB', raw_bin_data)
return {'sec_domain': u[0], 'key_set_version': u[1], 'counter': u[2], 'rfu': u[3]}
class EF_GP_DIV_DATA(LinFixedEF):
def __init__(self, fid='6f27', name='EF.GP_DIV_DATA', desc='GP SCP02 key diversification data'):
super().__init__(fid, name=name, desc=desc, rec_len=(12, 12))
def _decode_record_bin(self, raw_bin_data, **kwargs):
def _decode_record_bin(self, raw_bin_data):
u = unpack('!BB8s', raw_bin_data)
return {'sec_domain': u[0], 'key_set_version': u[1], 'key_div_data': u[2].hex()}
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),
'sres_deriv_func'/Mapping(Bit, {1:0, 2:1}),
'use_opc_instead_of_op'/Flag,
'use_sres_deriv_func_2'/Bit,
'use_opc_instead_of_op'/Bit,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
self._construct = Struct('cfg'/CfgByte,
'key'/HexAdapter(Bytes(16)),
'op_opc' /HexAdapter(Bytes(16)))
'op'/ If(this.cfg.algorithm == 'milenage' and not this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16))),
'opc' /
If(this.cfg.algorithm == 'milenage' and this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16)))
)
class EF_HTTPS_CFG(TransparentEF):
def __init__(self, fid='6f2a', name='EF.HTTPS_CFG'):
super().__init__(fid, name=name, desc='HTTPS configuration')
class EF_HTTPS_KEYS(TransparentEF):
KeyRecord = Struct('security_domain'/Int8ub,
'key_type'/Enum(Int8ub, des=0x80, psk=0x85, aes=0x88),
'key_version'/Int8ub,
'key_id'/Int8ub,
'key_length'/Int8ub,
'key'/HexAdapter(Bytes(this.key_length)))
def __init__(self, fid='6f2b', name='EF.HTTPS_KEYS'):
super().__init__(fid, name=name, desc='HTTPS PSK and DEK keys')
self._construct = GreedyRange(self.KeyRecord)
class EF_HTTPS_POLL(TransparentEF):
TimeUnit = Enum(Int8ub, seconds=0, minutes=1, hours=2, days=3, ten_days=4)
def __init__(self, fid='6f2c', name='EF.HTTPS_POLL'):
super().__init__(fid, name=name, desc='HTTPS polling interval')
self._construct = Struct(Const(b'\x82'), 'time_unit'/self.TimeUnit, 'value'/Int8ub,
'adm_session_triggering_tlv'/HexAdapter(GreedyBytes))
class DF_SYSTEM(CardDF):
@@ -177,29 +190,24 @@ class DF_SYSTEM(CardDF):
EF_0348_COUNT(),
EF_GP_COUNT(),
EF_GP_DIV_DATA(),
EF_HTTPS_CFG(),
EF_HTTPS_KEYS(),
EF_HTTPS_POLL(),
]
self.add_files(files)
def decode_select_response(self, resp_hex):
return CardProfileUICC.decode_select_response(resp_hex)
return pySim.ts_102_221.CardProfileUICC.decode_select_response(resp_hex)
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'/Flag, 'delta_max_check'/Flag,
'age_limit_check'/Flag, 'sqn_check'/Flag,
Flag1 = BitStruct('skip_next_sqn_check'/Bit, 'delta_max_check'/Bit,
'age_limit_check'/Bit, 'sqn_check'/Bit,
'ind_len'/BitsInteger(4))
Flag2 = BitStruct('rfu'/BitsRFU(5), 'dont_clear_amf_for_macs'/Flag,
'aus_concealed'/Flag, 'autn_concealed'/Flag)
Flag2 = BitStruct('rfu'/BitsRFU(5), 'dont_clear_amf_for_macs'/Bit,
'aus_concealed'/Bit, 'autn_concealed'/Bit)
self._construct = Struct('flag1'/Flag1, 'flag2'/Flag2,
'delta_max' /
BytesInteger(6), 'age_limit'/BytesInteger(6),
@@ -207,75 +215,39 @@ 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'/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)
CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Bit,
'use_sres_deriv_func_2_in_3g'/Bit,
'use_opc_instead_of_op'/Bit,
'algorithm'/Enum(Nibble, milenage=4, sha1_aka=5, xor=15))
self._construct = Struct('cfg'/CfgByte,
'key'/HexAdapter(Bytes(16)),
'op_opc' /HexAdapter(Bytes(16)))
# TUAK has a rather different layout for the data, so we define a different
# construct below and use explicit _{decode,encode}_bin() methods for separating
# the TUAK and non-TUAK situation
CfgByteTuak = BitStruct(Padding(1),
'key_length'/Mapping(Bit, {128:0, 256: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),
'ck_and_ik_size'/Mapping(Bit, {128:0, 256:1}),
'mac_size'/Mapping(BitsInteger(3), {64:0, 128:1, 256:2}),
'res_size'/Mapping(BitsInteger(3), {32:0, 64:1, 128:2, 256:3}))
self._constr_tuak = Struct('cfg'/CfgByteTuak,
'tuak_cfg'/TuakCfgByte,
'num_of_keccak_iterations'/Int8ub,
'op_opc'/HexAdapter(Bytes(32)),
'k'/HexAdapter(Bytes(this.cfg.key_length//8)))
def _decode_bin(self, raw_bin_data: bytearray) -> dict:
if raw_bin_data[0] & 0x0F == 0x06:
return parse_construct(self._constr_tuak, raw_bin_data)
else:
return parse_construct(self._construct, raw_bin_data)
def _encode_bin(self, abstract_data: dict, **kwargs) -> bytearray:
if abstract_data['cfg']['algorithm'] == 'tuak':
return build_construct(self._constr_tuak, abstract_data)
else:
return build_construct(self._construct, abstract_data)
'op' /
If(this.cfg.algorithm == 'milenage' and not this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16))),
'opc' /
If(this.cfg.algorithm == 'milenage' and this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16)))
)
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'/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))
CfgByte = BitStruct(Padding(1), 'only_4bytes_res_in_3g'/Bit,
'use_sres_deriv_func_2_in_3g'/Bit,
'use_opc_instead_of_op'/Bit,
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
self._construct = Struct('cfg'/CfgByte,
'key'/HexAdapter(Bytes(16)),
'op_opc' /HexAdapter(Bytes(16)))
'op' /
If(this.cfg.algorithm == 'milenage' and not this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16))),
'opc' /
If(this.cfg.algorithm == 'milenage' and this.cfg.use_opc_instead_of_op,
HexAdapter(Bytes(16)))
)
class EF_GBA_SK(TransparentEF):
@@ -328,34 +300,3 @@ class SysmocomSJA2(CardModel):
EF_USIM_SQN(name='EF.ISIM_SQN'),
]
isim_adf.add_files(files_adf_isim)
class SysmocomSJA5(CardModel):
_atrs = ["3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC",
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8",
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"]
@classmethod
def add_files(cls, rs: RuntimeState):
"""Add sysmocom SJA2 specific files to given RuntimeState."""
rs.mf.add_file(DF_SYSTEM())
# optional USIM application
if 'a0000000871002' in rs.mf.applications:
usim_adf = rs.mf.applications['a0000000871002']
files_adf_usim = [
EF_USIM_AUTH_KEY(),
EF_USIM_AUTH_KEY_2G(),
EF_GBA_SK(),
EF_GBA_REC_LIST(),
EF_GBA_INT_KEY(),
EF_USIM_SQN(),
]
usim_adf.add_files(files_adf_usim)
# optional ISIM application
if 'a0000000871004' in rs.mf.applications:
isim_adf = rs.mf.applications['a0000000871004']
files_adf_isim = [
EF_USIM_AUTH_KEY(name='EF.ISIM_AUTH_KEY'),
EF_USIM_AUTH_KEY_2G(name='EF.ISIM_AUTH_KEY_2G'),
EF_USIM_SQN(name='EF.ISIM_SQN'),
]
isim_adf.add_files(files_adf_isim)

439
pySim/tlv.py Normal file
View File

@@ -0,0 +1,439 @@
"""object-oriented TLV parser/encoder library."""
# (C) 2021 by Harald Welte <laforge@osmocom.org>
# All Rights Reserved
#
# 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, Any, Tuple
from bidict import bidict
from construct import *
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.construct import parse_construct, LV, HexAdapter, BcdAdapter, BitsRFU, GsmStringAdapter
from pySim.exceptions import *
import inspect
import abc
import re
def camel_to_snake(name):
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
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)
# 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))
nested = namespace.get('nested', kwargs.get('nested', None))
if nested is None or inspect.isclass(nested) and issubclass(nested, TLV_IE_Collection):
# caller has specified TLV_IE_Collection sub-class, we can directly reference it
x.nested_collection_cls = nested
else:
# caller passed list of other TLV classes that might possibly appear within us,
# build a dynamically-created TLV_IE_Collection sub-class and reference it
name = 'auto_collection_%s' % (name)
cls = type(name, (TLV_IE_Collection,), {'nested': nested})
x.nested_collection_cls = cls
return x
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)
# this becomes a _class_ variable, not an instance variable
x.possible_nested = namespace.get('nested', kwargs.get('nested', None))
return x
class Transcodable(abc.ABC):
_construct = None
"""Base class for something that can be encoded + encoded. Decoding and Encoding happens either
* via a 'construct' object stored in a derived class' _construct variable, or
* via a 'construct' object stored in an instance _construct variable, or
* via a derived class' _{to,from}_bytes() methods."""
def __init__(self):
self.encoded = None
self.decoded = None
self._construct = None
def to_bytes(self) -> bytes:
"""Convert from internal representation to binary bytes. Store the binary result
in the internal state and return it."""
if not self.decoded:
do = b''
elif self._construct:
do = self._construct.build(self.decoded, total_len=None)
elif self.__class__._construct:
do = self.__class__._construct.build(self.decoded, total_len=None)
else:
do = self._to_bytes()
self.encoded = do
return do
# not an abstractmethod, as it is only required if no _construct exists
def _to_bytes(self):
raise NotImplementedError('%s._to_bytes' % type(self).__name__)
def from_bytes(self, do: bytes):
"""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)
elif self.__class__._construct:
self.decoded = parse_construct(self.__class__._construct, do)
else:
self.decoded = self._from_bytes(do)
return self.decoded
# not an abstractmethod, as it is only required if no _construct exists
def _from_bytes(self, do: bytes):
raise NotImplementedError('%s._from_bytes' % type(self).__name__)
class IE(Transcodable, metaclass=TlvMeta):
# we specify the metaclass so any downstream subclasses will automatically use it
"""Base class for various Information Elements. We understand the notion of a hierarchy
of IEs on top of the Transcodable class."""
# this is overridden by the TlvMeta metaclass, if it is used to create subclasses
nested_collection_cls = None
tag = None
def __init__(self, **kwargs):
super().__init__()
self.nested_collection = None
if self.nested_collection_cls:
self.nested_collection = self.nested_collection_cls()
# if we are a constructed IE, [ordered] list of actual child-IE instances
self.children = kwargs.get('children', [])
self.decoded = kwargs.get('decoded', None)
def __repr__(self):
"""Return a string representing the [nested] IE data (for print)."""
if len(self.children):
member_strs = [repr(x) for x in self.children]
return '%s(%s)' % (type(self).__name__, ','.join(member_strs))
else:
return '%s(%s)' % (type(self).__name__, self.decoded)
def to_dict(self):
"""Return a JSON-serializable dict representing the [nested] IE data."""
if len(self.children):
v = [x.to_dict() for x in self.children]
else:
v = self.decoded
return {camel_to_snake(type(self).__name__): v}
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."""
if self.nested_collection:
self.children = self.nested_collection.from_dict(decoded)
else:
self.children = []
self.decoded = decoded
def is_constructed(self):
"""Is this IE constructed by further nested IEs?"""
if len(self.children):
return True
else:
return False
@abc.abstractmethod
def to_ie(self) -> 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."""
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()
return out
else:
return super().to_bytes()
def from_bytes(self, do: bytes):
"""Parse _the value part_ from binary bytes to internal representation."""
if self.nested_collection:
self.children = self.nested_collection.from_bytes(do)
else:
self.children = []
return super().from_bytes(do)
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
@classmethod
@abc.abstractmethod
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
"""Obtain the raw TAG at the start of the bytes provided by the user."""
@classmethod
@abc.abstractmethod
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
"""Obtain the length encoded at the start of the bytes provided by the user."""
@abc.abstractmethod
def _encode_tag(self) -> bytes:
"""Encode the tag part. Must be provided by derived (TLV format specific) class."""
@abc.abstractmethod
def _encode_len(self, val: bytes) -> bytes:
"""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_tlv(self):
"""Convert the internal representation to binary TLV bytes."""
val = self.to_bytes()
return self._encode_tag() + self._encode_len(val) + val
def from_tlv(self, do: bytes):
if len(do) == 0:
return {}, b''
(rawtag, remainder) = self.__class__._parse_tag_raw(do)
if rawtag:
if rawtag != self._compute_tag():
raise ValueError("%s: Encountered tag %s doesn't match our supported tag %s" %
(self, rawtag, self.tag))
(length, remainder) = self.__class__._parse_len(remainder)
value = remainder[:length]
remainder = remainder[length:]
else:
value = do
remainder = b''
dec = self.from_bytes(value)
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)
@classmethod
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
return bertlv_parse_tag_raw(do)
@classmethod
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
return bertlv_parse_len(do)
def _encode_tag(self) -> bytes:
return bertlv_encode_tag(self._compute_tag())
def _encode_len(self, val: bytes) -> bytes:
return bertlv_encode_len(len(val))
class COMPR_TLV_IE(TLV_IE):
"""TLV_IE formated as COMPREHENSION-TLV as described in ETSI TS 101 220."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.comprehension = False
@classmethod
def _decode_tag(cls, do: bytes) -> Tuple[dict, bytes]:
return comprehensiontlv_parse_tag(do)
@classmethod
def _parse_tag_raw(cls, do: bytes) -> Tuple[int, bytes]:
return comprehensiontlv_parse_tag_raw(do)
@classmethod
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
return bertlv_parse_len(do)
def _encode_tag(self) -> bytes:
return comprehensiontlv_encode_tag(self._compute_tag())
def _encode_len(self, val: bytes) -> bytes:
return bertlv_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.
A given encoded DO may contain any of them in any order, and may contain multiple instances
of each DO."""
# this is overridden by the TlvCollectionMeta metaclass, if it is used to create subclasses
possible_nested = []
def __init__(self, desc=None, **kwargs):
self.desc = desc
#print("possible_nested: ", self.possible_nested)
self.members = kwargs.get('nested', self.possible_nested)
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}
# if we are a constructed IE, [ordered] list of actual child-IE instances
self.children = kwargs.get('children', [])
self.encoded = None
def __str__(self):
member_strs = [str(x) for x in self.members]
return '%s(%s)' % (type(self).__name__, ','.join(member_strs))
def __repr__(self):
member_strs = [repr(x) for x in self.members]
return '%s(%s)' % (self.__class__, ','.join(member_strs))
def __add__(self, other):
"""Extending TLV_IE_Collections with other TLV_IE_Collections or TLV_IEs."""
if isinstance(other, TLV_IE_Collection):
# adding one collection to another
members = self.members + other.members
return TLV_IE_Collection(self.desc, nested=members)
elif inspect.isclass(other) and issubclass(other, TLV_IE):
# adding a member to a collection
return TLV_IE_Collection(self.desc, nested=self.members + [other])
else:
raise TypeError
def from_bytes(self, binary: bytes) -> List[TLV_IE]:
"""Create a list of TLV_IEs from the collection based on binary input data.
Args:
binary : binary bytes of encoded data
Returns:
list of instances of TLV_IE sub-classes containing parsed data
"""
self.encoded = binary
# list of instances of TLV_IE collection member classes appearing in the data
res = []
remainder = binary
first = next(iter(self.members_by_tag.values()))
# iterate until no binary trailer is left
while len(remainder):
# obtain the tag at the start of the remainder
tag, r = first._parse_tag_raw(remainder)
if tag == None:
return res
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)
res.append(inst)
else:
# unknown tag; create the related class on-the-fly using the same base class
name = 'unknown_%s_%X' % (first.__base__.__name__, tag)
cls = type(name, (first.__base__,), {'tag': tag, 'possible_nested': [],
'nested_collection_cls': None})
cls._from_bytes = lambda s, a: {'raw': a.hex()}
cls._to_bytes = lambda s: bytes.fromhex(s.decoded['raw'])
# create an instance and parse accordingly
inst = cls()
dec, remainder = inst.from_tlv(remainder)
res.append(inst)
self.children = res
return res
def from_dict(self, decoded: List[dict]) -> List[TLV_IE]:
"""Create a list of TLV_IE instances from the collection based on an array
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 = []
for i in decoded:
for k in i.keys():
if k in self.members_by_name:
cls = self.members_by_name[k]
inst = cls()
inst.from_dict(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.children = res
return res
def to_dict(self):
return [x.to_dict() for x in self.children]
def to_bytes(self):
out = b''
for c in self.children:
out += c.to_tlv()
return out
def from_tlv(self, do):
return self.from_bytes(do)
def to_tlv(self):
return self.to_bytes()
def flatten_dict_lists(inp):
"""hierarchically flatten each list-of-dicts into a single dict. This is useful to
make the output of hierarchical TLV decoder structures flatter and more easy to read."""
def are_all_elements_dict(l):
for e in l:
if not isinstance(e, dict):
return False
return True
if isinstance(inp, list):
if are_all_elements_dict(inp):
# flatten into one shared dict
newdict = {}
for e in inp:
key = list(e.keys())[0]
newdict[key] = e[key]
inp = newdict
# process result as any native dict
return {k:flatten_dict_lists(inp[k]) for k in inp.keys()}
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()}
else:
return inp

Some files were not shown because too many files have changed in this diff Show More