11 Commits

Author SHA1 Message Date
Harald Welte
890e1951fe Implement Global Platform SCP03
This adds an implementation of the GlobalPlatform SCP03 protocol. It has
been tested in S8 mode for C-MAC, C-ENC, R-MAC and R-ENC with AES using
128, 192 and 256 bit key lengh.  Test vectors generated while talking to
a sysmoEUICC1-C2T are included as unit tests.

Change-Id: Ibc35af5474923aed2e3bcb29c8d713b4127a160d
2024-02-04 15:11:08 +01:00
Harald Welte
dd6895ff06 rename global_platform.scp02 to global_platform.scp
This is in preparation of extending it to cover SCP03 in a follow-up
patch.

Change-Id: Idc0afac6e95f89ddaf277a89f9c95607e70a471c
2024-02-04 15:06:50 +01:00
Harald Welte
3a0f881d84 Contstrain argparse integers to permitted range
In many casese we used type=int permitting any integer value, positive
or negative without a constratint in size.  However, in reality often
we're constrained to unsigned 8 or 16 bit ranges.  Let's use the
auto_uint{8,16} functions to enforce this within argparse before
we even try to encode something that won't work.

Change-Id: I35c81230bc18e2174ec1930aa81463f03bcd69c8
2024-02-04 14:53:07 +01:00
Harald Welte
4b23130da9 global_platform: Fix --key-id argument
The key-id is actually a 7-bit integer and on the wire the 8th bit
has a special meaning which can be derived automatically.

Let's unburden the user from explicitly encoding that 8th bit and
instead set it automatically.

Change-Id: I8da37aa8fd064e6d35ed29a70f5d7a0e9060be3a
2024-02-04 14:53:07 +01:00
Harald Welte
c84c04afb5 global_platform: add delete_key and delete_card_content
This GlobalPlatform command is used to delete applications/load-files
or keys.

Change-Id: Ib5d18e983d0e918633d7c090c54fb9a3384a22e5
2024-02-04 14:53:07 +01:00
Harald Welte
0e9ad7b5d4 global_platform: add set_status command
Using this command, one can change the life cycle status of on-card
applications, specifically one can LOCK (disable) them and re-enable
them as needed.

Change-Id: Ie14297a119d01cad1284f315a2508aa92cb4633b
2024-02-04 14:53:07 +01:00
Harald Welte
5dc8471526 global_platform: Add install_for_personalization command
This allows us to perform STORE DATA on applications like ARA-M/ARA-D
after establishing SCP02 to the related security domain.

Change-Id: I2ce766b97bba42c64c4d4492b505be66c24f471e
2024-02-04 14:53:07 +01:00
Harald Welte
5bbd720512 pySim-shell: Make 'apdu' command use logical (and secure) channel
The 'apdu' command so far bypassed the logical channel and also
the recently-introduced support for secure channels.  Let's change
that, at least by default.  If somebody wants a raw APDU without
secure / logical channel processing, they may use the --raw option.

Change-Id: Id0c364f772c31e11e8dfa21624d8685d253220d0
2024-02-04 14:53:07 +01:00
Harald Welte
71a7a19159 SCP02: Only C-MAC/C-ENCRYPT APDUs whose CLA byte indicates GlobalPlatform
I'm not entirely sure if this is the right thing to do.  For sure I do
have cards which don't like SELECT with C-MAC appended... and
GlobalPlatform clearly states SELECT is coded with CLA value that has
the MSB not set (i.e. not a GlobalPlatform command).

Change-Id: Ieda75c865a6ff2725fc3c8772bb274d96b8a5a43
2024-02-04 14:53:07 +01:00
Harald Welte
ca3678e471 Add global_platform shell command establish_scp02 and release_scp
Those commands can be used to establish and release a SCP02 secure
channel on the currently active logical channel.

The prompt is adjusted with a 'SCP02' prefix while the secure channel is
established.

Change-Id: Ib2f3c8f0563f81a941dd55b97c9836e3a6856407
2024-02-04 14:53:07 +01:00
Harald Welte
1e052f1efa Introduce GlobalPlatform SCP02 implementation
This implementation of GlobalPlatform SCP02 currently only supports
C-MAC and C-ENC, but no R-MAC or R-ENC yet.

The patch also introduces the notion of having a SCP instance associated
with a SimCardCommands instance.  No code is using this yet, it will be
introduced in a separate patch.

Change-Id: I56020382b9dfe8ba0f7c1c9f71eb1a9746bc5a27
2024-02-04 14:53:07 +01:00
332 changed files with 5646 additions and 62205 deletions

1
.github/FUNDING.yml vendored
View File

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

13
.gitignore vendored
View File

@@ -1,16 +1,5 @@
*.pyc
.*.sw?
.*.swp
/docs/_*
/docs/generated
/.cache
/.local
/build
/pySim.egg-info
/smdpp-data/sm-dp-sessions*
dist
tags
smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.pem
smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_BRP.pem
smdpp-data/generated
smdpp-data/certs/dhparam2048.pem

View File

@@ -1,29 +1,16 @@
pySim - Tools for reading, decoding, browsing SIM/USIM/ISIM/HPSIM/eUICC Cards
=============================================================================
pySim - Read, Write and Browse Programmable SIM/USIM/ISIM/HPSIM Cards
=====================================================================
This repository contains a number of Python programs related to working with
subscriber identity modules of cellular networks, including but not limited
to SIM, UICC, USIM, ISIM, HPSIMs and eUICCs.
* `pySim-shell.py` can be used to interactively explore, read and decode contents
of any of the supported card models / card applications. Furthermore, if
you have the credentials to your card (ADM PIN), you can also write to the card,
i.e. edit its contents.
* `pySim-read.py` and `pySim-prog.py` are _legacy_ tools for batch programming
some very common parameters to an entire batch of programmable cards
* `pySim-trace.py` is a tool to do an in-depth decode of SIM card protocol traces
such as those obtained by [Osmocom SIMtrace2](https://osmocom.org/projects/simtrace2/wiki)
or [osmo-qcdiag](https://osmocom.org/projects/osmo-qcdiag/wiki).
* `osmo-smdpp.py` is a proof-of-concept GSMA SGP.22 Consumer eSIM SM-DP+ for lab/research
* there are more related tools, particularly in the `contrib` directory.
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.
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 as the various
[sysmocom programmable card products](https://shop.sysmocom.de/SIM/).
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
@@ -62,9 +49,9 @@ pySim-shell vs. legacy tools
----------------------------
While you will find a lot of online resources still describing the use of
`pySim-prog.py` and `pySim-read.py`, those tools are considered legacy by
pySim-prog.py and pySim-read.py, those tools are considered legacy by
now and have by far been superseded by the much more capable
`pySim-shell.py`. We strongly encourage users to adopt pySim-shell, unless
pySim-shell. We strongly encourage users to adopt pySim-shell, unless
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.
@@ -90,7 +77,7 @@ Please install the following dependencies:
- cmd2 >= 1.5.0
- colorlog
- construct >= 2.9.51
- pyosmocom
- gsm0338
- jsonpath-ng
- packaging
- pycryptodomex
@@ -136,34 +123,19 @@ 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
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.
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
------------

View File

@@ -1,112 +0,0 @@
#!/usr/bin/env python3
# A tool to analyze the eUICC simaResponse (series of EUICCResponse)
#
# (C) 2025 by sysmocom - s.f.m.c. GmbH
# 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/>.
#
import argparse
from osmocom.utils import h2b, b2h
from osmocom.tlv import bertlv_parse_one, bertlv_encode_tag, bertlv_encode_len
from pySim.esim.saip import *
parser = argparse.ArgumentParser(description="""Utility program to analyze the contents of an eUICC simaResponse.""")
parser.add_argument('SIMA_RESPONSE', help='Hexstring containing the simaResponse as received from the eUICC')
def split_sima_response(sima_response):
"""split an eUICC simaResponse field into a list of EUICCResponse fields"""
remainder = sima_response
result = []
while len(remainder):
tdict, l, v, next_remainder = bertlv_parse_one(remainder)
rawtag = bertlv_encode_tag(tdict)
rawlen = bertlv_encode_len(l)
result = result + [remainder[0:len(rawtag) + len(rawlen) + l]]
remainder = next_remainder
return result
def analyze_status(status):
"""
Convert a status code (integer) into a human readable string
(see eUICC Profile Package: Interoperable Format Technical Specification, section 8.11)
"""
# SIMA status codes
string_values = {0 : 'ok',
1 : 'pe-not-supported',
2 : 'memory-failure',
3 : 'bad-values',
4 : 'not-enough-memory',
5 : 'invalid-request-format',
6 : 'invalid-parameter',
7 : 'runtime-not-supported',
8 : 'lib-not-supported',
9 : 'template-not-supported ',
10 : 'feature-not-supported',
11 : 'pin-code-missing',
31 : 'unsupported-profile-version'}
string_value = string_values.get(status, None)
if string_value is not None:
return "%d = %s (SIMA status code)" % (status, string_value)
# ISO 7816 status words
if status >= 24576 and status <= 28671:
return "%d = %04x (ISO7816 status word)" % (status, status)
elif status >= 36864 and status <= 40959:
return "%d = %04x (ISO7816 status word)" % (status, status)
# Proprietary status codes
elif status >= 40960 and status <= 65535:
return "%d = %04x (proprietary)" % (status, status)
# Unknown status codes
return "%d (unknown, proprietary?)" % status
def analyze_euicc_response(euicc_response):
"""Analyze and display the contents of an EUICCResponse"""
print(" EUICCResponse: %s" % b2h(euicc_response))
euicc_response_decoded = asn1.decode('EUICCResponse', euicc_response)
pe_status = euicc_response_decoded.get('peStatus')
print(" peStatus:")
for s in pe_status:
print(" status: %s" % analyze_status(s.get('status')))
print(" identification: %s" % str(s.get('identification', None)))
print(" additional-information: %s" % str(s.get('additional-information', None)))
print(" offset: %s" % str(s.get('offset', None)))
if euicc_response_decoded.get('profileInstallationAborted', False) is None:
# This type is defined as profileInstallationAborted NULL OPTIONAL, so when it is present it
# will have the value None, otherwise it is simply not present.
print(" profileInstallationAborted: True")
else:
print(" profileInstallationAborted: False")
status_message = euicc_response_decoded.get('statusMessage', None)
print(" statusMessage: %s" % str(status_message))
if __name__ == '__main__':
opts = parser.parse_args()
sima_response = h2b(opts.SIMA_RESPONSE);
print("simaResponse: %s" % b2h(sima_response))
euicc_response_list = split_sima_response(sima_response)
for euicc_response in euicc_response_list:
analyze_euicc_response(euicc_response)

View File

@@ -1,66 +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 CardKeyFieldCryptor
class CsvColumnEncryptor(CardKeyFieldCryptor):
def __init__(self, filename: str, transport_keys: dict):
self.filename = filename
self.crypt = CardKeyFieldCryptor(transport_keys)
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 fieldname in cr.fieldnames:
row[fieldname] = self.crypt.encrypt_field(fieldname, row[fieldname])
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)
cce = CsvColumnEncryptor(opts.CSVFILE, csv_column_keys)
cce.encrypt()

View File

@@ -1,304 +0,0 @@
#!/usr/bin/env python3
# (C) 2025 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import logging
import csv
import sys
import os
import yaml
import psycopg2
from psycopg2.sql import Identifier, SQL
from pathlib import Path
from pySim.log import PySimLogger
from packaging import version
log = PySimLogger.get(Path(__file__).stem)
class CardKeyDatabase:
def __init__(self, config_filename: str, table_name: str, create_table: bool = False, admin: bool = False):
"""
Initialize database connection and set the table which shall be used as storage for the card key data.
In case the specified table does not exist yet it can be created using the create_table_type parameter.
New tables are always minimal tables which follow a pre-defined table scheme. The user may extend the table
with additional columns using the add_cols() later.
Args:
tablename : name of the database table to create.
create_table_type : type of the table to create ('UICC' or 'EUICC')
"""
def user_from_config_file(config, role: str) -> tuple[str, str]:
db_users = config.get('db_users')
user = db_users.get(role)
if user is None:
raise ValueError("user for role '%s' not set up in config file." % role)
return user.get('name'), user.get('pass')
self.table = table_name.lower()
self.cols = None
# Depending on the table type, the table name must contain either the substring "uicc_keys" or "euicc_keys".
# This convention will allow us to deduct the table type from the table name.
if "euicc_keys" not in table_name and "uicc_keys" not in table_name:
raise ValueError("Table name (%s) should contain the substring \"uicc_keys\" or \"euicc_keys\"" % table_name)
# Read config file
log.info("Using config file: %s", config_filename)
with open(config_filename, "r") as cfg:
config = yaml.load(cfg, Loader=yaml.FullLoader)
host = config.get('host')
log.info("Database host: %s", host)
db_name = config.get('db_name')
log.info("Database name: %s", db_name)
table_names = config.get('table_names')
username_admin, password_admin = user_from_config_file(config, 'admin')
username_importer, password_importer = user_from_config_file(config, 'importer')
username_reader, _ = user_from_config_file(config, 'reader')
# Switch between admin and importer user
if admin:
username, password = username_admin, password_admin
else:
username, password = username_importer, password_importer
# Create database connection
log.info("Database user: %s", username)
self.conn = psycopg2.connect(dbname=db_name, user=username, password=password, host=host)
self.cur = self.conn.cursor()
# In the context of this tool it is not relevant if the table name is present in the config file. However,
# pySim-shell.py will require the table name to be configured properly to access the database table.
if self.table not in table_names:
log.warning("Specified table name (%s) is not yet present in config file (required for access from pySim-shell.py)",
self.table)
# Create a new minimal database table of the specified table type.
if create_table:
if not admin:
raise ValueError("creation of new table refused, use option --admin and try again.")
if "euicc_keys" in self.table:
self.__create_table(username_reader, username_importer, ['EID'])
elif "uicc_keys" in self.table:
self.__create_table(username_reader, username_importer, ['ICCID', 'IMSI'])
# Ensure a table with the specified name exists
log.info("Database table: %s", self.table)
if self.get_cols() == []:
raise ValueError("Table name (%s) does not exist yet" % self.table)
log.info("Database table columns: %s", str(self.get_cols()))
def __create_table(self, user_reader:str, user_importer:str, cols:list[str]):
"""
Initialize a new table. New tables are always minimal tables with one primary key and additional index columns.
Non index-columns may be added later using method _update_cols().
"""
# Create table columns with primary key
query = SQL("CREATE TABLE {} ({} VARCHAR PRIMARY KEY").format(Identifier(self.table),
Identifier(cols[0].lower()))
for c in cols[1:]:
query += SQL(", {} VARCHAR").format(Identifier(c.lower()))
query += SQL(");")
self.cur.execute(query)
# Create indexes for all other columns
for c in cols[1:]:
self.cur.execute(query = SQL("CREATE INDEX {} ON {}({});").format(Identifier(c.lower()),
Identifier(self.table),
Identifier(c.lower())))
# Set permissions
self.cur.execute(SQL("GRANT INSERT ON {} TO {};").format(Identifier(self.table),
Identifier(user_importer)))
self.cur.execute(SQL("GRANT SELECT ON {} TO {};").format(Identifier(self.table),
Identifier(user_reader)))
log.info("New database table created: %s", self.table)
def get_cols(self) -> list[str]:
"""
Get a list of all columns available in the current table scheme.
Returns:
list with column names (in uppercase) of the database table
"""
# Return cached col list if present
if self.cols:
return self.cols
# Request a list of current cols from the database
self.cur.execute("SELECT column_name FROM information_schema.columns where table_name = %s;", (self.table,))
cols_result = self.cur.fetchall()
cols = []
for c in cols_result:
cols.append(c[0].upper())
self.cols = cols
return cols
def get_missing_cols(self, cols_expected:list[str]) -> list[str]:
"""
Check if the current table scheme lacks any of the given expected columns.
Returns:
list with the missing columns.
"""
cols_present = self.get_cols()
return list(set(cols_expected) - set(cols_present))
def add_cols(self, cols:list[str]):
"""
Update the current table scheme with additional columns. In case the updated columns are already exist, the
table schema is not changed.
Args:
table : name of the database table to alter
cols : list with updated colum names to add
"""
cols_missing = self.get_missing_cols(cols)
# Depending on the table type (see constructor), we either have a primary key 'ICCID' (for UICC data), or 'EID'
# (for eUICC data). Both table formats different types of data and have rather differen columns also. Let's
# prevent the excidentally mixing of both types.
if 'ICCID' in cols_missing:
raise ValueError("Table %s stores eUCCC key material, refusing to add UICC specific column 'ICCID'" % self.table)
if 'EID' in cols_missing:
raise ValueError("Table %s stores UCCC key material, refusing to add eUICC specific column 'EID'" % self.table)
# Add the missing columns to the table
self.cols = None
for c in cols_missing:
self.cur.execute(query = SQL("ALTER TABLE {} ADD {} VARCHAR;").format(Identifier(self.table),
Identifier(c.lower())))
def insert_row(self, row:dict[str, str]):
"""
Insert a new row into the database table.
Args:
row : dictionary with the colum names and their designated values
"""
# Check if the row is compatible with the current table scheme
cols_expected = list(row.keys())
cols_missing = self.get_missing_cols(cols_expected)
if cols_missing != []:
raise ValueError("table %s has incompatible format, the row %s contains unknown cols %s" %
(self.table, str(row), str(cols_missing)))
# Insert row into datbase table
row_keys = list(row.keys())
row_values = list(row.values())
query = SQL("INSERT INTO {} ").format(Identifier(self.table))
query += SQL("({} ").format(Identifier(row_keys[0].lower()))
for k in row_keys[1:]:
query += SQL(", {}").format(Identifier(k.lower()))
query += SQL(") VALUES (%s")
for v in row_values[1:]:
query += SQL(", %s")
query += SQL(");")
self.cur.execute(query, row_values)
def commit(self):
self.conn.commit()
log.info("Changes to table %s committed!", self.table)
def open_csv(opts: argparse.Namespace):
log.info("CSV file: %s", opts.csv)
csv_file = open(opts.csv, 'r')
cr = csv.DictReader(csv_file)
if not cr:
raise RuntimeError("could not open DictReader for CSV-File '%s'" % opts.csv)
cr.fieldnames = [field.upper() for field in cr.fieldnames]
log.info("CSV file columns: %s", str(cr.fieldnames))
return cr
def open_db(cr: csv.DictReader, opts: argparse.Namespace) -> CardKeyDatabase:
try:
db = CardKeyDatabase(os.path.expanduser(opts.pgsql), opts.table_name, opts.create_table, opts.admin)
# Check CSV format against table schema, add missing columns
cols_missing = db.get_missing_cols(cr.fieldnames)
if cols_missing != [] and (opts.update_columns or opts.create_table):
log.info("Adding missing columns: %s", str(cols_missing))
db.add_cols(cols_missing)
cols_missing = db.get_missing_cols(cr.fieldnames)
# Make sure the table schema has no missing columns
if cols_missing != []:
log.error("Database table lacks CSV file columns: %s -- import aborted!", cols_missing)
sys.exit(2)
except Exception as e:
log.error(str(e).strip())
log.error("Database initialization aborted due to error!")
sys.exit(2)
return db
def import_from_csv(db: CardKeyDatabase, cr: csv.DictReader):
count = 0
for row in cr:
try:
db.insert_row(row)
count+=1
if count % 100 == 0:
log.info("CSV file import in progress, %d rows imported...", count)
except Exception as e:
log.error(str(e).strip())
log.error("CSV file import aborted due to error, no datasets committed!")
sys.exit(2)
log.info("CSV file import done, %d rows imported", count)
if __name__ == '__main__':
option_parser = argparse.ArgumentParser(description='CSV importer for pySim-shell\'s PostgreSQL Card Key Provider',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
option_parser.add_argument('--pgsql', metavar='FILE',
default="~/.osmocom/pysim/card_data_pgsql.cfg",
help='Read card data from PostgreSQL database (config file)')
option_parser.add_argument('--csv', metavar='FILE', help='input CSV file with card data', required=True)
option_parser.add_argument("--table-name", help="name of the card key table", type=str, required=True)
option_parser.add_argument("--update-columns", help="add missing table columns", action='store_true', default=False)
option_parser.add_argument("--create-table", action='store_true', help="create new card key table", default=False)
option_parser.add_argument("--admin", action='store_true', help="perform action as admin", default=False)
opts = option_parser.parse_args()
PySimLogger.setup(print, {logging.WARN: "\033[33m"})
if (opts.verbose):
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
# Open CSV file
cr = open_csv(opts)
# Open database, create initial table, update column scheme
db = open_db(cr, opts)
# Progress with import
if not opts.admin:
import_from_csv(db, cr)
# Commit changes to the database
db.commit()

View File

@@ -1,84 +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, ActivationCode
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 manually 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 should 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)
matchingId = res.get('matchingId', None)
smdpAddress = res.get('smdpAddress', None)
if matchingId:
ac = ActivationCode(smdpAddress, matchingId, cc_required=bool(opts.confirmationCode))
print("Activation Code: '%s'" % ac.to_string())
elif opts.command == 'cancel-order':
res = peer.call_cancelOrder(data)
elif opts.command == 'release-profile':
res = peer.call_releaseProfile(data)

View File

@@ -1,100 +0,0 @@
#!/usr/bin/env python3
# (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 sys
import argparse
import logging
import json
import asn1tools
import asn1tools.codecs.ber
import asn1tools.codecs.der
import pySim.esim.rsp as rsp
import pySim.esim.saip as saip
from pySim.esim.es2p import param, Es2pApiServerMno, Es2pApiServerHandlerMno
from osmocom.utils import b2h
from datetime import datetime
from analyze_simaResponse import split_sima_response
from pathlib import Path
logger = logging.getLogger(Path(__file__).stem)
parser = argparse.ArgumentParser(description="""
Utility to receive and log requests against the ES2+ API of an SM-DP+ according to GSMA SGP.22.""")
parser.add_argument("--host", help="Host/IP to bind HTTP(S) to", default="localhost")
parser.add_argument("--port", help="TCP port to bind HTTP(S) to", default=443, type=int)
parser.add_argument('--server-cert', help='X.509 server certificate used to provide the ES2+ HTTPs service')
parser.add_argument('--client-ca-cert', help='X.509 CA certificates to authenticate the requesting client(s)')
parser.add_argument("-v", "--verbose", help="enable debug output", action='store_true', default=False)
def decode_sima_response(sima_response):
decoded = []
euicc_response_list = split_sima_response(sima_response)
for euicc_response in euicc_response_list:
decoded.append(saip.asn1.decode('EUICCResponse', euicc_response))
return decoded
def decode_result_data(result_data):
return rsp.asn1.decode('PendingNotification', result_data)
def decode(data, path="/"):
if data is None:
return 'none'
elif type(data) is datetime:
return data.isoformat()
elif type(data) is tuple:
return {str(data[0]) : decode(data[1], path + str(data[0]) + "/")}
elif type(data) is list:
new_data = []
for item in data:
new_data.append(decode(item, path))
return new_data
elif type(data) is bytes:
return b2h(data)
elif type(data) is dict:
new_data = {}
for key, item in data.items():
new_key = str(key)
if path == '/' and new_key == 'resultData':
new_item = decode_result_data(item)
elif (path == '/resultData/profileInstallationResult/profileInstallationResultData/finalResult/successResult/' \
or path == '/resultData/profileInstallationResult/profileInstallationResultData/finalResult/errorResult/') \
and new_key == 'simaResponse':
new_item = decode_sima_response(item)
else:
new_item = item
new_data[new_key] = decode(new_item, path + new_key + "/")
return new_data
else:
return data
class Es2pApiServerHandlerForLogging(Es2pApiServerHandlerMno):
def call_handleDownloadProgressInfo(self, data: dict) -> (dict, str):
logging.info("ES2+:handleDownloadProgressInfo: %s" % json.dumps(decode(data)))
return {}, None
if __name__ == "__main__":
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING,
format='%(asctime)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
Es2pApiServerMno(args.port, args.host, Es2pApiServerHandlerForLogging(), args.server_cert, args.client_ca_cert)

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 Operation 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 self.opts.iccid:
ntf_metadata['iccid'] = h2b(swap_nibbles(self.opts.iccid))
if self.opts.operation == 'install':
pird = {
'transactionId': h2b(self.opts.transaction_id),
'notificationMetadata': ntf_metadata,
'smdpOid': self.opts.smdpp_oid,
'finalResult': ('successResult', {
'aid': h2b(self.opts.isdp_aid),
'simaResponse': h2b(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,48 +0,0 @@
#!/usr/bin/env python3
# Small command line utility program to encode eSIM QR-Codes
# (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 sys
import argparse
from pySim.esim import ActivationCode
option_parser = argparse.ArgumentParser(description="""
eSIM QR code generator. Will encode the given hostname + activation code
into the eSIM RSP String format as specified in SGP.22 Section 4.1. If
a PNG output file is specified, it will also generate a QR code.""")
option_parser.add_argument('hostname', help='FQDN of SM-DP+')
option_parser.add_argument('token', help='MatchingID / Token')
option_parser.add_argument('--oid', help='SM-DP+ OID in CERT.DPauth.ECDSA')
option_parser.add_argument('--confirmation-code-required', action='store_true',
help='Whether a Confirmation Code is required')
option_parser.add_argument('--png', help='Output PNG file name (no PNG is written if omitted)')
if __name__ == '__main__':
opts = option_parser.parse_args()
ac = ActivationCode(opts.hostname, opts.token, opts.oid, opts.confirmation_code_required)
print(ac.to_string())
if opts.png:
with open(opts.png, 'wb') as f:
img = ac.to_qrcode()
img.save(f)
print("# generated QR code stored to '%s'" % (opts.png))

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env python3
# (C) 2025 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 argparse
from osmocom.utils import h2b, swap_nibbles
from pySim.esim.es8p import ProfileMetadata
parser = argparse.ArgumentParser(description="""Utility program to generate profile metadata in the
StoreMetadataRequest format based on input values from the command line.""")
parser.add_argument('--iccid', required=True, help="ICCID of eSIM profile");
parser.add_argument('--spn', required=True, help="Service Provider Name");
parser.add_argument('--profile-name', required=True, help="eSIM Profile Name");
parser.add_argument('--profile-class', choices=['test', 'operational', 'provisioning'],
default='operational', help="Profile Class");
parser.add_argument('--outfile', required=True, help="Output File Name");
if __name__ == '__main__':
opts = parser.parse_args()
iccid_bin = h2b(swap_nibbles(opts.iccid))
pmd = ProfileMetadata(iccid_bin, spn=opts.spn, profile_name=opts.profile_name,
profile_class=opts.profile_class)
with open(opts.outfile, 'wb') as f:
f.write(pmd.gen_store_metadata_request())
print("Written StoreMetadataRequest to '%s'" % opts.outfile)

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,661 +0,0 @@
#!/usr/bin/env python3
"""
Faithfully reproduces the smdpp certs contained in SGP.26_v1.5_Certificates_18_07_2024.zip
available at https://www.gsma.com/solutions-and-impact/technologies/esim/gsma_resources/sgp-26-test-certificate-definition-v1-5/
Only usable for testing, it obviously uses a different CI key.
"""
import os
import binascii
from datetime import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
# Custom OIDs used in certificates
OID_CERTIFICATE_POLICIES_CI = "2.23.146.1.2.1.0" # CI cert policy
OID_CERTIFICATE_POLICIES_TLS = "2.23.146.1.2.1.3" # DPtls cert policy
OID_CERTIFICATE_POLICIES_AUTH = "2.23.146.1.2.1.4" # DPauth cert policy
OID_CERTIFICATE_POLICIES_PB = "2.23.146.1.2.1.5" # DPpb cert policy
# Subject Alternative Name OIDs
OID_CI_RID = "2.999.1" # CI Registered ID
OID_DP_RID = "2.999.10" # DP+ Registered ID
OID_DP2_RID = "2.999.12" # DP+2 Registered ID
OID_DP4_RID = "2.999.14" # DP+4 Registered ID
OID_DP8_RID = "2.999.18" # DP+8 Registered ID
class SimplifiedCertificateGenerator:
def __init__(self):
self.backend = default_backend()
# Store generated CI keys to sign other certs
self.ci_certs = {} # {"BRP": cert, "NIST": cert}
self.ci_keys = {} # {"BRP": key, "NIST": key}
def get_curve(self, curve_type):
"""Get the appropriate curve object."""
if curve_type == "BRP":
return ec.BrainpoolP256R1()
else:
return ec.SECP256R1()
def generate_key_pair(self, curve):
"""Generate a new EC key pair."""
private_key = ec.generate_private_key(curve, self.backend)
return private_key
def load_private_key_from_hex(self, hex_key, curve):
"""Load EC private key from hex string."""
key_bytes = binascii.unhexlify(hex_key.replace(":", "").replace(" ", "").replace("\n", ""))
key_int = int.from_bytes(key_bytes, 'big')
return ec.derive_private_key(key_int, curve, self.backend)
def generate_ci_cert(self, curve_type):
"""Generate CI certificate for either BRP or NIST curve."""
curve = self.get_curve(curve_type)
private_key = self.generate_key_pair(curve)
# Build subject and issuer (self-signed) - same for both
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, "Test CI"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "TESTCERT"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSPTEST"),
x509.NameAttribute(NameOID.COUNTRY_NAME, "IT"),
])
# Build certificate - all parameters same for both
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(issuer)
builder = builder.not_valid_before(datetime(2020, 4, 1, 8, 27, 51))
builder = builder.not_valid_after(datetime(2055, 4, 1, 8, 27, 51))
builder = builder.serial_number(0xb874f3abfa6c44d3)
builder = builder.public_key(private_key.public_key())
# Add extensions - all same for both
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier(OID_CERTIFICATE_POLICIES_CI),
policy_qualifiers=None
)
]),
critical=True
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=False,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=True,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.SubjectAlternativeName([
x509.RegisteredID(x509.ObjectIdentifier(OID_CI_RID))
]),
critical=False
)
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
),
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
)
]),
critical=False
)
certificate = builder.sign(private_key, hashes.SHA256(), self.backend)
self.ci_keys[curve_type] = private_key
self.ci_certs[curve_type] = certificate
return certificate, private_key
def generate_dp_cert(self, curve_type, subject_cn, serial, key_hex,
cert_policy_oid, rid_oid, validity_start, validity_end):
"""Generate a DP certificate signed by CI - works for both BRP and NIST."""
curve = self.get_curve(curve_type)
private_key = self.load_private_key_from_hex(key_hex, curve)
ci_cert = self.ci_certs[curve_type]
ci_key = self.ci_keys[curve_type]
subject = x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ACME"),
x509.NameAttribute(NameOID.COMMON_NAME, subject_cn),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(ci_cert.subject)
builder = builder.not_valid_before(validity_start)
builder = builder.not_valid_after(validity_end)
builder = builder.serial_number(serial)
builder = builder.public_key(private_key.public_key())
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectAlternativeName([
x509.RegisteredID(x509.ObjectIdentifier(rid_oid))
]),
critical=False
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier(cert_policy_oid),
policy_qualifiers=None
)
]),
critical=True
)
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
),
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
)
]),
critical=False
)
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
return certificate, private_key
def generate_tls_cert(self, curve_type, subject_cn, dns_name, serial, key_hex,
rid_oid, validity_start, validity_end):
"""Generate a TLS certificate signed by CI."""
curve = self.get_curve(curve_type)
private_key = self.load_private_key_from_hex(key_hex, curve)
ci_cert = self.ci_certs[curve_type]
ci_key = self.ci_keys[curve_type]
subject = x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ACME"),
x509.NameAttribute(NameOID.COMMON_NAME, subject_cn),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(ci_cert.subject)
builder = builder.not_valid_before(validity_start)
builder = builder.not_valid_after(validity_end)
builder = builder.serial_number(serial)
builder = builder.public_key(private_key.public_key())
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH
]),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier(OID_CERTIFICATE_POLICIES_TLS),
policy_qualifiers=None
)
]),
critical=False
)
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectAlternativeName([
x509.DNSName(dns_name),
x509.RegisteredID(x509.ObjectIdentifier(rid_oid))
]),
critical=False
)
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
),
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
)
]),
critical=False
)
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
return certificate, private_key
def generate_eum_cert(self, curve_type, key_hex):
"""Generate EUM certificate signed by CI."""
curve = self.get_curve(curve_type)
private_key = self.load_private_key_from_hex(key_hex, curve)
ci_cert = self.ci_certs[curve_type]
ci_key = self.ci_keys[curve_type]
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "ES"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
x509.NameAttribute(NameOID.COMMON_NAME, "EUM Test"),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(ci_cert.subject)
builder = builder.not_valid_before(datetime(2020, 4, 1, 9, 28, 37))
builder = builder.not_valid_after(datetime(2054, 3, 24, 9, 28, 37))
builder = builder.serial_number(0x12345678)
builder = builder.public_key(private_key.public_key())
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=False,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier("2.23.146.1.2.1.2"), # EUM policy
policy_qualifiers=None
)
]),
critical=True
)
builder = builder.add_extension(
x509.SubjectAlternativeName([
x509.RegisteredID(x509.ObjectIdentifier("2.999.5"))
]),
critical=False
)
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=0),
critical=True
)
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
relative_name=None,
reasons=None,
crl_issuer=None
)
]),
critical=False
)
# Name Constraints
constrained_name = x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
x509.NameAttribute(NameOID.SERIAL_NUMBER, "89049032"),
])
name_constraints = x509.NameConstraints(
permitted_subtrees=[
x509.DirectoryName(constrained_name)
],
excluded_subtrees=None
)
builder = builder.add_extension(
name_constraints,
critical=True
)
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
return certificate, private_key
def generate_euicc_cert(self, curve_type, eum_cert, eum_key, key_hex):
"""Generate eUICC certificate signed by EUM."""
curve = self.get_curve(curve_type)
private_key = self.load_private_key_from_hex(key_hex, curve)
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "ES"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
x509.NameAttribute(NameOID.SERIAL_NUMBER, "89049032123451234512345678901235"),
x509.NameAttribute(NameOID.COMMON_NAME, "Test eUICC"),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(eum_cert.subject)
builder = builder.not_valid_before(datetime(2020, 4, 1, 9, 48, 58))
builder = builder.not_valid_after(datetime(7496, 1, 24, 9, 48, 58))
builder = builder.serial_number(0x0200000000000001)
builder = builder.public_key(private_key.public_key())
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(eum_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False
)
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False
),
critical=True
)
builder = builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.ObjectIdentifier("2.23.146.1.2.1.1"), # eUICC policy
policy_qualifiers=None
)
]),
critical=True
)
certificate = builder.sign(eum_key, hashes.SHA256(), self.backend)
return certificate, private_key
def save_cert_and_key(self, cert, key, cert_path_der, cert_path_pem, key_path_sk, key_path_pk):
"""Save certificate and key in various formats."""
# Create directories if needed
os.makedirs(os.path.dirname(cert_path_der), exist_ok=True)
with open(cert_path_der, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.DER))
if cert_path_pem:
with open(cert_path_pem, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
if key and key_path_sk:
with open(key_path_sk, "wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
if key and key_path_pk:
with open(key_path_pk, "wb") as f:
f.write(key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
))
def main():
gen = SimplifiedCertificateGenerator()
output_dir = "smdpp-data/generated"
os.makedirs(output_dir, exist_ok=True)
print("=== Generating CI Certificates ===")
for curve_type in ["BRP", "NIST"]:
ci_cert, ci_key = gen.generate_ci_cert(curve_type)
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
ci_cert, ci_key,
f"{output_dir}/CertificateIssuer/CERT_CI{suffix}.der",
f"{output_dir}/CertificateIssuer/CERT_CI{suffix}.pem",
None, None
)
print(f"Generated CI {curve_type} certificate")
print("\n=== Generating DPauth Certificates ===")
dpauth_configs = [
("BRP", "TEST SM-DP+", 256, "93:fb:33:d0:58:4f:34:9b:07:f8:b5:d2:af:93:d7:c3:e3:54:b3:49:a3:b9:13:50:2e:6a:bc:07:0e:4d:49:29", OID_DP_RID, "DPauth"),
("NIST", "TEST SM-DP+", 256, "0a:7c:c1:c2:44:e6:0c:52:cd:5b:78:07:ab:8c:36:0c:26:52:46:01:50:7d:ca:bc:5d:d5:98:b5:a6:16:d5:d5", OID_DP_RID, "DPauth"),
("BRP", "TEST SM-DP+2", 512, "0c:17:35:5c:01:1d:0f:e8:d7:da:dd:63:f1:97:85:cf:6c:51:cb:cd:46:6a:e8:8b:e8:f8:1b:c1:05:88:46:f6", OID_DP2_RID, "DP2auth"),
("NIST", "TEST SM-DP+2", 512, "9c:32:a0:95:d4:88:42:d9:ff:a4:04:f7:12:51:2a:a2:c5:42:5a:1a:26:38:6a:b6:a1:45:d5:81:1e:03:91:41", OID_DP2_RID, "DP2auth"),
]
for curve_type, cn, serial, key_hex, rid_oid, name_prefix in dpauth_configs:
cert, key = gen.generate_dp_cert(
curve_type, cn, serial, key_hex,
OID_CERTIFICATE_POLICIES_AUTH, rid_oid,
datetime(2020, 4, 1, 8, 31, 30),
datetime(2030, 3, 30, 8, 31, 30)
)
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
cert, key,
f"{output_dir}/DPauth/CERT_S_SM_{name_prefix}{suffix}.der",
None,
f"{output_dir}/DPauth/SK_S_SM_{name_prefix}{suffix}.pem",
f"{output_dir}/DPauth/PK_S_SM_{name_prefix}{suffix}.pem"
)
print(f"Generated {name_prefix} {curve_type} certificate")
print("\n=== Generating DPpb Certificates ===")
dppb_configs = [
("BRP", "TEST SM-DP+", 257, "75:ff:32:2f:41:66:16:da:e1:a4:84:ef:71:d4:87:4f:b0:df:32:95:fd:35:c2:cb:a4:89:fb:b2:bb:9c:7b:f6", OID_DP_RID, "DPpb"),
("NIST", "TEST SM-DP+", 257, "dc:d6:94:b7:78:95:7e:8e:9a:dd:bd:d9:44:33:e9:ef:8f:73:d1:1e:49:1c:48:d4:25:a3:8a:94:91:bd:3b:ed", OID_DP_RID, "DPpb"),
("BRP", "TEST SM-DP+2", 513, "9c:ae:2e:1a:56:07:a9:d5:78:38:2e:ee:93:2e:25:1f:52:30:4f:86:ee:b1:f1:70:8c:db:d3:c0:7b:e2:cd:3d", OID_DP2_RID, "DP2pb"),
("NIST", "TEST SM-DP+2", 513, "66:93:11:49:63:9d:ba:ac:1d:c3:d3:06:c5:8b:d2:df:d2:2f:73:bf:63:ac:86:31:98:32:90:b5:7f:90:93:45", OID_DP2_RID, "DP2pb"),
]
for curve_type, cn, serial, key_hex, rid_oid, name_prefix in dppb_configs:
cert, key = gen.generate_dp_cert(
curve_type, cn, serial, key_hex,
OID_CERTIFICATE_POLICIES_PB, rid_oid,
datetime(2020, 4, 1, 8, 34, 46),
datetime(2030, 3, 30, 8, 34, 46)
)
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
cert, key,
f"{output_dir}/DPpb/CERT_S_SM_{name_prefix}{suffix}.der",
None,
f"{output_dir}/DPpb/SK_S_SM_{name_prefix}{suffix}.pem",
f"{output_dir}/DPpb/PK_S_SM_{name_prefix}{suffix}.pem"
)
print(f"Generated {name_prefix} {curve_type} certificate")
print("\n=== Generating DPtls Certificates ===")
dptls_configs = [
("BRP", "testsmdpplus1.example.com", "testsmdpplus1.example.com", 9, "3f:67:15:28:02:b3:f4:c7:fa:e6:79:58:55:f6:82:54:1e:45:e3:5e:ff:f4:e8:a0:55:65:a0:f1:91:2a:78:2e", OID_DP_RID, "DP_TLS_BRP"),
("NIST", "testsmdpplus1.example.com", "testsmdpplus1.example.com", 9, "a0:3e:7c:e4:55:04:74:be:a4:b7:a8:73:99:ce:5a:8c:9f:66:1b:68:0f:94:01:39:ff:f8:4e:9d:ec:6a:4d:8c", OID_DP_RID, "DP_TLS_NIST"),
("NIST", "testsmdpplus2.example.com", "testsmdpplus2.example.com", 12, "4e:65:61:c6:40:88:f6:69:90:7a:db:e3:94:b1:1a:84:24:2e:03:3a:82:a8:84:02:31:63:6d:c9:1b:4e:e3:f5", OID_DP2_RID, "DP2_TLS"),
("NIST", "testsmdpplus4.example.com", "testsmdpplus4.example.com", 14, "f2:65:9d:2f:52:8f:4b:11:37:40:d5:8a:0d:2a:f3:eb:2b:48:e1:22:c2:b6:0a:6a:f6:fc:96:ad:86:be:6f:a4", OID_DP4_RID, "DP4_TLS"),
("NIST", "testsmdpplus8.example.com", "testsmdpplus8.example.com", 18, "ff:6e:4a:50:9b:ad:db:38:10:88:31:c2:3c:cc:2d:44:30:7a:f2:81:e9:25:96:7f:8c:df:1d:95:54:a0:28:8d", OID_DP8_RID, "DP8_TLS"),
]
for curve_type, cn, dns, serial, key_hex, rid_oid, name_prefix in dptls_configs:
cert, key = gen.generate_tls_cert(
curve_type, cn, dns, serial, key_hex, rid_oid,
datetime(2024, 7, 9, 15, 29, 36),
datetime(2025, 8, 11, 15, 29, 36)
)
gen.save_cert_and_key(
cert, key,
f"{output_dir}/DPtls/CERT_S_SM_{name_prefix}.der",
None,
f"{output_dir}/DPtls/SK_S_SM_{name_prefix.replace('_BRP', '_BRP').replace('_NIST', '_NIST')}.pem",
f"{output_dir}/DPtls/PK_S_SM_{name_prefix.replace('_BRP', '_BRP').replace('_NIST', '_NIST')}.pem"
)
print(f"Generated {name_prefix} certificate")
print("\n=== Generating EUM Certificates ===")
eum_configs = [
("BRP", "12:9b:0a:b1:3f:17:e1:4a:40:b6:fa:4e:d8:23:e0:cf:46:5b:7b:3d:73:24:05:e6:29:5d:3b:23:b0:45:c9:9a"),
("NIST", "25:e6:75:77:28:e1:e9:51:13:51:9c:dc:34:55:5c:29:ba:ed:23:77:3a:c5:af:dd:dc:da:d9:84:89:8a:52:f0"),
]
eum_certs = {}
eum_keys = {}
for curve_type, key_hex in eum_configs:
cert, key = gen.generate_eum_cert(curve_type, key_hex)
eum_certs[curve_type] = cert
eum_keys[curve_type] = key
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
cert, key,
f"{output_dir}/EUM/CERT_EUM{suffix}.der",
None,
f"{output_dir}/EUM/SK_EUM{suffix}.pem",
f"{output_dir}/EUM/PK_EUM{suffix}.pem"
)
print(f"Generated EUM {curve_type} certificate")
print("\n=== Generating eUICC Certificates ===")
euicc_configs = [
("BRP", "8d:c3:47:a7:6d:b7:bd:d6:22:2d:d7:5e:a1:a1:68:8a:ca:81:1e:4c:bc:6a:7f:6a:ef:a4:b2:64:19:62:0b:90"),
("NIST", "11:e1:54:67:dc:19:4f:33:71:83:e4:60:c9:f6:32:60:09:1e:12:e8:10:26:cd:65:61:e1:7c:6d:85:39:cc:9c"),
]
for curve_type, key_hex in euicc_configs:
cert, key = gen.generate_euicc_cert(curve_type, eum_certs[curve_type], eum_keys[curve_type], key_hex)
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
gen.save_cert_and_key(
cert, key,
f"{output_dir}/eUICC/CERT_EUICC{suffix}.der",
None,
f"{output_dir}/eUICC/SK_EUICC{suffix}.pem",
f"{output_dir}/eUICC/PK_EUICC{suffix}.pem"
)
print(f"Generated eUICC {curve_type} certificate")
print("\n=== Certificate generation complete! ===")
print(f"All certificates saved to: {output_dir}/")
if __name__ == "__main__":
main()

View File

@@ -4,23 +4,18 @@
# 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)
# * JOB_TYPE: one of 'test', 'pylint', 'docs'
#
export PYTHONUNBUFFERED=1
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
fi
case "$JOB_TYPE" in
"test")
virtualenv -p python3 venv --system-site-packages
@@ -30,42 +25,16 @@ case "$JOB_TYPE" in
pip install pyshark
# Execute automatically discovered unit tests first
python -m unittest discover -v -s tests/unittests
python -m unittest discover -v -s tests/
# 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/
# Run pySim-smpp2sim test
tests/pySim-smpp2sim_test/pySim-smpp2sim_test.sh
;;
"distcheck")
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install .
pip install pyshark
for prog in venv/bin/pySim-*.py; do
$prog --help > /dev/null
done
# Run the test with physical cards
cd pysim-testdata
../tests/pySim-prog_test.sh
../tests/pySim-trace_test.sh
;;
"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)
@@ -76,15 +45,9 @@ case "$JOB_TYPE" in
--disable E1102 \
--disable E0401 \
--enable W0301 \
pySim tests/unittests/*.py *.py \
contrib/*.py
pySim *.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
@@ -97,5 +60,3 @@ case "$JOB_TYPE" in
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
exit 1
esac
osmo-clean-workspace.sh

View File

@@ -1,489 +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
from pathlib import Path as PlPath
from typing import List
from osmocom.utils import h2b, b2h, swap_nibbles
from osmocom.construct import GreedyBytes, StripHeaderAdapter
from pySim.esim.saip import *
from pySim.esim.saip.validation import CheckBasicStructure
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('extract-pe', help='Extract specified PE to (DER encoded) file')
parser_rpe.add_argument('--pe-file', required=True, help='PE file name')
parser_rpe.add_argument('--identification', type=int, help='Extract PE matching specified identification')
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', default=[], type=int, action='append', help='Remove PEs matching specified identification')
parser_rpe.add_argument('--type', default=[], action='append', help='Remove PEs matching specified type')
parser_rn = subparsers.add_parser('remove-naa', help='Remove specified 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_info.add_argument('--apps', action='store_true', help='List applications and their related instances')
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_aapp = subparsers.add_parser('add-app', help='Add application to PE-Sequence')
parser_aapp.add_argument('--output-file', required=True, help='Output file name')
parser_aapp.add_argument('--applet-file', required=True, help='Applet file name')
parser_aapp.add_argument('--aid', required=True, help='Load package AID')
parser_aapp.add_argument('--sd-aid', default=None, help='Security Domain AID')
parser_aapp.add_argument('--non-volatile-code-limit', default=None, type=int, help='Non volatile code limit (C6)')
parser_aapp.add_argument('--volatile-data-limit', default=None, type=int, help='Volatile data limit (C7)')
parser_aapp.add_argument('--non-volatile-data-limit', default=None, type=int, help='Non volatile data limit (C8)')
parser_aapp.add_argument('--hash-value', default=None, help='Hash value')
parser_rapp = subparsers.add_parser('remove-app', help='Remove application from PE-Sequence')
parser_rapp.add_argument('--output-file', required=True, help='Output file name')
parser_rapp.add_argument('--aid', required=True, help='Load package AID')
parser_aappi = subparsers.add_parser('add-app-inst', help='Add application instance to Application PE')
parser_aappi.add_argument('--output-file', required=True, help='Output file name')
parser_aappi.add_argument('--aid', required=True, help='Load package AID')
parser_aappi.add_argument('--class-aid', required=True, help='Class AID')
parser_aappi.add_argument('--inst-aid', required=True, help='Instance AID (must match Load package AID)')
parser_aappi.add_argument('--app-privileges', default='000000', help='Application privileges')
parser_aappi.add_argument('--volatile-memory-quota', default=None, type=int, help='Volatile memory quota (C7)')
parser_aappi.add_argument('--non-volatile-memory-quota', default=None, type=int, help='Non volatile memory quota (C8)')
parser_aappi.add_argument('--app-spec-pars', default='00', help='Application specific parameters (C9)')
parser_aappi.add_argument('--uicc-toolkit-app-spec-pars', help='UICC toolkit application specific parameters field')
parser_aappi.add_argument('--uicc-access-app-spec-pars', help='UICC Access application specific parameters field')
parser_aappi.add_argument('--uicc-adm-access-app-spec-pars', help='UICC Administrative access application specific parameters field')
parser_aappi.add_argument('--process-data', default=[], action='append', help='Process personalization APDUs')
parser_rappi = subparsers.add_parser('remove-app-inst', help='Remove application instance from Application PE')
parser_rappi.add_argument('--output-file', required=True, help='Output file name')
parser_rappi.add_argument('--aid', required=True, help='Load package AID')
parser_rappi.add_argument('--inst-aid', required=True, help='Instance AID')
esrv_flag_choices = [t.name for t in asn1.types['ServicesList'].type.root_members]
parser_esrv = subparsers.add_parser('edit-mand-srv-list', help='Add/Remove service flag from/to mandatory services list')
parser_esrv.add_argument('--output-file', required=True, help='Output file name')
parser_esrv.add_argument('--add-flag', default=[], choices=esrv_flag_choices, action='append', help='Add flag to mandatory services list')
parser_esrv.add_argument('--remove-flag', default=[], choices=esrv_flag_choices, action='append', help='Remove flag from mandatory services list')
parser_tree = subparsers.add_parser('tree', help='Display the filesystem tree')
def write_pes(pes: ProfileElementSequence, output_file:str):
"""write the PE sequence to a file"""
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), output_file))
with open(output_file, 'wb') as f:
f.write(pes.to_der())
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_extract_pe(pes: ProfileElementSequence, opts):
new_pe_list = []
for pe in pes.pe_list:
if pe.identification == opts.identification:
print("Extracting PE %s (id=%u) to file %s..." % (pe, pe.identification, opts.pe_file))
with open(opts.pe_file, 'wb') as f:
f.write(pe.to_der())
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
if pe.type in opts.type:
print("Removing PE %s (type=%s) from Sequence..." % (pe, pe.type))
continue
new_pe_list.append(pe)
pes.pe_list = new_pe_list
pes._process_pelist()
write_pes(pes, opts.output_file)
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)
write_pes(pes, opts.output_file)
def info_apps(pes:ProfileElementSequence):
def show_member(dictionary:Optional[dict], member:str, indent:str="\t", mandatory:bool = False, limit:bool = False):
if dictionary is None:
return
value = dictionary.get(member, None)
if value is None and mandatory == True:
print("%s%s: (missing!)" % (indent, member))
return
elif value is None:
return
if limit and len(value) > 40:
print("%s%s: '%s...%s' (%u bytes)" % (indent, member, b2h(value[:20]), b2h(value[-20:]), len(value)))
else:
print("%s%s: '%s' (%u bytes)" % (indent, member, b2h(value), len(value)))
apps = pes.pe_by_type.get('application', [])
if len(apps) == 0:
print("No Application PE present!")
return;
for app_pe in enumerate(apps):
print("Application #%u:" % app_pe[0])
print("\tloadBlock:")
load_block = app_pe[1].decoded['loadBlock']
show_member(load_block, 'loadPackageAID', "\t\t", True)
show_member(load_block, 'securityDomainAID', "\t\t")
show_member(load_block, 'nonVolatileCodeLimitC6', "\t\t")
show_member(load_block, 'volatileDataLimitC7', "\t\t")
show_member(load_block, 'nonVolatileDataLimitC8', "\t\t")
show_member(load_block, 'hashValue', "\t\t")
show_member(load_block, 'loadBlockObject', "\t\t", True, True)
for inst in enumerate(app_pe[1].decoded.get('instanceList', [])):
print("\tinstanceList[%u]:" % inst[0])
show_member(inst[1], 'applicationLoadPackageAID', "\t\t", True)
if inst[1].get('applicationLoadPackageAID', None) != load_block.get('loadPackageAID', None):
print("\t\t(applicationLoadPackageAID should be the same as loadPackageAID!)")
show_member(inst[1], 'classAID', "\t\t", True)
show_member(inst[1], 'instanceAID', "\t\t", True)
show_member(inst[1], 'extraditeSecurityDomainAID', "\t\t")
show_member(inst[1], 'applicationPrivileges', "\t\t", True)
show_member(inst[1], 'lifeCycleState', "\t\t", True)
show_member(inst[1], 'applicationSpecificParametersC9', "\t\t", True)
sys_specific_pars = inst[1].get('systemSpecificParameters', None)
if sys_specific_pars:
print("\t\tsystemSpecificParameters:")
show_member(sys_specific_pars, 'volatileMemoryQuotaC7', "\t\t\t")
show_member(sys_specific_pars, 'nonVolatileMemoryQuotaC8', "\t\t\t")
show_member(sys_specific_pars, 'globalServiceParameters', "\t\t\t")
show_member(sys_specific_pars, 'implicitSelectionParameter', "\t\t\t")
show_member(sys_specific_pars, 'volatileReservedMemory', "\t\t\t")
show_member(sys_specific_pars, 'nonVolatileReservedMemory', "\t\t\t")
show_member(sys_specific_pars, 'ts102226SIMFileAccessToolkitParameter', "\t\t\t")
additional_cl_pars = inst.get('ts102226AdditionalContactlessParameters', None)
if additional_cl_pars:
print("\t\t\tts102226AdditionalContactlessParameters:")
show_member(additional_cl_pars, 'protocolParameterData', "\t\t\t\t")
show_member(sys_specific_pars, 'userInteractionContactlessParameters', "\t\t\t")
show_member(sys_specific_pars, 'cumulativeGrantedVolatileMemory', "\t\t\t")
show_member(sys_specific_pars, 'cumulativeGrantedNonVolatileMemory', "\t\t\t")
app_pars = inst[1].get('applicationParameters', None)
if app_pars:
print("\t\tapplicationParameters:")
show_member(app_pars, 'uiccToolkitApplicationSpecificParametersField', "\t\t\t")
show_member(app_pars, 'uiccAccessApplicationSpecificParametersField', "\t\t\t")
show_member(app_pars, 'uiccAdministrativeAccessApplicationSpecificParametersField', "\t\t\t")
ctrl_ref_tp = inst[1].get('controlReferenceTemplate', None)
if ctrl_ref_tp:
print("\t\tcontrolReferenceTemplate:")
show_member(ctrl_ref_tp, 'applicationProviderIdentifier', "\t\t\t", True)
process_data = inst[1].get('processData', None)
if process_data:
print("\t\tprocessData:")
for proc in process_data:
print("\t\t\t" + b2h(proc))
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
if opts.apps:
info_apps(pes)
return;
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("\t%s" % repr(key))
# 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))
print("Writing Load Package AID: %s to file %s" % (package_aid, fname))
app_pe.to_file(fname)
def do_add_app(pes:ProfileElementSequence, opts):
print("Applying applet file: '%s'..." % opts.applet_file)
app_pe = ProfileElementApplication.from_file(opts.applet_file,
opts.aid,
opts.sd_aid,
opts.non_volatile_code_limit,
opts.volatile_data_limit,
opts.non_volatile_data_limit,
opts.hash_value)
security_domain = pes.pe_by_type.get('securityDomain', [])
if len(security_domain) == 0:
print("profile package does not contain a securityDomain, please add a securityDomain PE first!")
elif len(security_domain) > 1:
print("adding an application PE to profiles with multiple securityDomain is not supported yet!")
else:
pes.insert_after_pe(security_domain[0], app_pe)
print("application PE inserted into PE Sequence after securityDomain PE AID: %s" %
b2h(security_domain[0].decoded['instance']['instanceAID']))
write_pes(pes, opts.output_file)
def do_remove_app(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
if opts.aid == package_aid:
identification = app_pe.identification
opts_remove_pe = argparse.Namespace()
opts_remove_pe.identification = [app_pe.identification]
opts_remove_pe.type = []
opts_remove_pe.output_file = opts.output_file
print("Found Load Package AID: %s, removing related PE (id=%u) from Sequence..." %
(package_aid, identification))
do_remove_pe(pes, opts_remove_pe)
return
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
def do_add_app_inst(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
if opts.aid == package_aid:
print("Found Load Package AID: %s, adding new instance AID: %s to Application PE..." %
(opts.aid, opts.inst_aid))
app_pe.add_instance(opts.aid,
opts.class_aid,
opts.inst_aid,
opts.app_privileges,
opts.app_spec_pars,
opts.uicc_toolkit_app_spec_pars,
opts.uicc_access_app_spec_pars,
opts.uicc_adm_access_app_spec_pars,
opts.volatile_memory_quota,
opts.non_volatile_memory_quota,
opts.process_data)
write_pes(pes, opts.output_file)
return
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
def do_remove_app_inst(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
if opts.aid == b2h(app_pe.decoded['loadBlock']['loadPackageAID']):
print("Found Load Package AID: %s, removing instance AID: %s from Application PE..." %
(opts.aid, opts.inst_aid))
app_pe.remove_instance(opts.inst_aid)
write_pes(pes, opts.output_file)
return
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
def do_edit_mand_srv_list(pes: ProfileElementSequence, opts):
header = pes.pe_by_type.get('header', [])[0]
for s in opts.add_flag:
print("Adding service '%s' to mandatory services list..." % s)
header.mandatory_service_add(s)
for s in opts.remove_flag:
if s in header.decoded['eUICC-Mandatory-services'].keys():
print("Removing service '%s' from mandatory services list..." % s)
header.mandatory_service_remove(s)
else:
print("Service '%s' not present in mandatory services list, cannot remove!" % s)
print("The following services are now set mandatory:")
for s in header.decoded['eUICC-Mandatory-services'].keys():
print("\t%s" % s)
write_pes(pes, opts.output_file)
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 == 'extract-pe':
do_extract_pe(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 == 'add-app':
do_add_app(pes, opts)
elif opts.command == 'remove-app':
do_remove_app(pes, opts)
elif opts.command == 'add-app-inst':
do_add_app_inst(pes, opts)
elif opts.command == 'remove-app-inst':
do_remove_app_inst(pes, opts)
elif opts.command == 'edit-mand-srv-list':
do_edit_mand_srv_list(pes, opts)
elif opts.command == 'tree':
do_tree(pes, opts)

View File

@@ -1,31 +0,0 @@
#!/bin/bash
# This is an example script to illustrate how to add JAVA card applets to an existing eUICC profile package.
PYSIMPATH=../
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE.der
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
APPPATH=./HelloSTK_09122024.cap
# Download example applet (see also https://gitea.osmocom.org/sim-card/hello-stk):
if ! [ -f $APPPATH ]; then
wget https://osmocom.org/attachments/download/8931/HelloSTK_09122024.cap
fi
# Step #1: Create the application PE and load the ijc contents from the .cap file:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH add-app \
--output-file $OUTPATH --applet-file $APPPATH --aid 'D07002CA44'
# Step #2: Create the application instance inside the application PE created in step #1:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH add-app-inst --output-file $OUTPATH \
--aid 'D07002CA44' \
--class-aid 'D07002CA44900101' \
--inst-aid 'D07002CA44900101' \
--app-privileges '00' \
--app-spec-pars '00' \
--uicc-toolkit-app-spec-pars '01001505000000000000000000000000'
# Display the contents of the resulting application PE:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps
# For an explanation of --uicc-toolkit-app-spec-pars, see:
# ETSI TS 102 226, section 8.2.1.3.2.2.1

View File

@@ -1,8 +0,0 @@
#!/bin/bash
# This is an example script to illustrate how to extract JAVA card applets from an existing eUICC profile package.
PYSIMPATH=../
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
OUTPATH=./
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH extract-apps --output-dir ./ --format ijc

View File

@@ -1,14 +0,0 @@
#!/bin/bash
# This is an example script to illustrate how to remove a JAVA card applet instance from an application PE inside an
# existing eUICC profile package.
PYSIMPATH=../
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello-no-inst.der
# Remove application PE entirely
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH remove-app-inst \
--output-file $OUTPATH --aid 'd07002ca44' --inst-aid 'd07002ca44900101'
# Display the contents of the resulting application PE:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps

View File

@@ -1,13 +0,0 @@
#!/bin/bash
# This is an example script to illustrate how to remove a JAVA card applet from an existing eUICC profile package.
PYSIMPATH=../
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-no-hello.der
# Remove application PE entirely
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH remove-app \
--output-file $OUTPATH --aid 'D07002CA44'
# Display the contents of the resulting application PE:
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps

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

@@ -1,239 +0,0 @@
#!/usr/bin/env python3
# (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Harald Welte, Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import logging
import smpplib.gsm
import smpplib.client
import smpplib.consts
import time
from pySim.ota import OtaKeyset, OtaDialectSms, OtaAlgoCrypt, OtaAlgoAuth, CNTR_REQ, RC_CC_DS, POR_REQ
from pySim.utils import b2h, h2b, is_hexstr
from pathlib import Path
logger = logging.getLogger(Path(__file__).stem)
class SmppHandler:
client = None
def __init__(self, host: str, port: int,
system_id: str, password: str,
ota_keyset: OtaKeyset, spi: dict, tar: bytes):
"""
Initialize connection to SMPP server and set static OTA SMS-TPDU ciphering parameters
Args:
host : Hostname or IPv4/IPv6 address of the SMPP server
port : TCP Port of the SMPP server
system_id: SMPP System-ID used by ESME (client) to bind
password: SMPP Password used by ESME (client) to bind
ota_keyset: OTA keyset to be used for SMS-TPDU ciphering
spi: Security Parameter Indicator (SPI) to be used for SMS-TPDU ciphering
tar: Toolkit Application Reference (TAR) of the targeted card application
"""
# Create and connect SMPP client
client = smpplib.client.Client(host, port, allow_unknown_opt_params=True)
client.set_message_sent_handler(self.message_sent_handler)
client.set_message_received_handler(self.message_received_handler)
client.connect()
client.bind_transceiver(system_id=system_id, password=password)
self.client = client
# Setup static OTA parameters
self.ota_dialect = OtaDialectSms()
self.ota_keyset = ota_keyset
self.tar = tar
self.spi = spi
def __del__(self):
if self.client:
self.client.unbind()
self.client.disconnect()
def message_received_handler(self, pdu):
if pdu.short_message:
logger.info("SMS-TPDU received: %s", b2h(pdu.short_message))
try:
dec = self.ota_dialect.decode_resp(self.ota_keyset, self.spi, pdu.short_message)
except ValueError:
# Retry to decoding with ciphering disabled (in case the card has problems to decode the SMS-TDPU
# we have sent, the response will contain an unencrypted error message)
spi = self.spi.copy()
spi['por_shall_be_ciphered'] = False
spi['por_rc_cc_ds'] = 'no_rc_cc_ds'
dec = self.ota_dialect.decode_resp(self.ota_keyset, spi, pdu.short_message)
logger.info("SMS-TPDU decoded: %s", dec)
self.response = dec
return None
def message_sent_handler(self, pdu):
logger.debug("SMS-TPDU sent: pdu_sequence=%s pdu_message_id=%s", pdu.sequence, pdu.message_id)
def transceive_sms_tpdu(self, tpdu: bytes, src_addr: str, dest_addr: str, timeout: int) -> tuple:
"""
Transceive SMS-TPDU. This method sends the SMS-TPDU to the SMPP server, and waits for a response. The method
returns when the response is received.
Args:
tpdu : short message content (plaintext)
src_addr : short message source address
dest_addr : short message destination address
timeout : timeout after which this method should give up waiting for a response
Returns:
tuple containing the response (plaintext)
"""
logger.info("SMS-TPDU sending: %s...", b2h(tpdu))
self.client.send_message(
# TODO: add parameters to switch source_addr_ton and dest_addr_ton between SMPP_TON_INTL and SMPP_NPI_ISDN
source_addr_ton=smpplib.consts.SMPP_TON_INTL,
source_addr=src_addr,
dest_addr_ton=smpplib.consts.SMPP_TON_INTL,
destination_addr=dest_addr,
short_message=tpdu,
# TODO: add parameters to set data_coding and esm_class
data_coding=smpplib.consts.SMPP_ENCODING_BINARY,
esm_class=smpplib.consts.SMPP_GSMFEAT_UDHI,
protocol_id=0x7f,
# TODO: add parameter to use registered delivery
# registered_delivery=True,
)
logger.info("SMS-TPDU sent, waiting for response...")
timestamp_sent=int(time.time())
self.response = None
while self.response is None:
self.client.poll()
if int(time.time()) - timestamp_sent > timeout:
raise ValueError("Timeout reached, no response SMS-TPDU received!")
return self.response
def transceive_apdu(self, apdu: bytes, src_addr: str, dest_addr: str, timeout: int) -> tuple[bytes, bytes]:
"""
Transceive APDU. This method wraps the given APDU into an SMS-TPDU, sends it to the SMPP server and waits for
the response. When the response is received, the last response data and the last status word is extracted from
the response and returned to the caller.
Args:
apdu : one or more concatenated APDUs
src_addr : short message source address
dest_addr : short message destination address
timeout : timeout after which this method should give up waiting for a response
Returns:
tuple containing the last response data and the last status word as byte strings
"""
logger.info("C-APDU sending: %s...", b2h(apdu))
# translate to Secured OTA RFM
secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu)
# add user data header
tpdu = b'\x02\x70\x00' + secured
# send via SMPP
response = self.transceive_sms_tpdu(tpdu, src_addr, dest_addr, timeout)
# Extract last_response_data and last_status_word from the response
sw = None
resp = None
for container in response:
if container:
container_dict = dict(container)
resp = container_dict.get('last_response_data')
sw = container_dict.get('last_status_word')
if resp is None:
raise ValueError("Response does not contain any last_response_data, no R-APDU received!")
if sw is None:
raise ValueError("Response does not contain any last_status_word, no R-APDU received!")
logger.info("R-APDU received: %s %s", resp, sw)
return h2b(resp), h2b(sw)
if __name__ == '__main__':
option_parser = argparse.ArgumentParser(description='CSV importer for pySim-shell\'s PostgreSQL Card Key Provider',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
option_parser.add_argument("--host", help="Host/IP of the SMPP server", default="localhost")
option_parser.add_argument("--port", help="TCP port of the SMPP server", default=2775, type=int)
option_parser.add_argument("--system-id", help="System ID to use to bind to the SMPP server", default="test")
option_parser.add_argument("--password", help="Password to use to bind to the SMPP server", default="test")
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
algo_crypt_choices = []
algo_crypt_classes = OtaAlgoCrypt.__subclasses__()
for cls in algo_crypt_classes:
algo_crypt_choices.append(cls.enum_name)
option_parser.add_argument("--algo-crypt", choices=algo_crypt_choices, default='triple_des_cbc2',
help="OTA crypt algorithm")
algo_auth_choices = []
algo_auth_classes = OtaAlgoAuth.__subclasses__()
for cls in algo_auth_classes:
algo_auth_choices.append(cls.enum_name)
option_parser.add_argument("--algo-auth", choices=algo_auth_choices, default='triple_des_cbc2',
help="OTA auth algorithm")
option_parser.add_argument('--kic', required=True, type=is_hexstr, help='OTA key (KIC)')
option_parser.add_argument('--kic_idx', default=1, type=int, help='OTA key index (KIC)')
option_parser.add_argument('--kid', required=True, type=is_hexstr, help='OTA key (KID)')
option_parser.add_argument('--kid_idx', default=1, type=int, help='OTA key index (KID)')
option_parser.add_argument('--cntr', default=0, type=int, help='replay protection counter')
option_parser.add_argument('--tar', required=True, type=is_hexstr, help='Toolkit Application Reference')
option_parser.add_argument("--cntr_req", choices=CNTR_REQ.decmapping.values(), default='no_counter',
help="Counter requirement")
option_parser.add_argument('--no-ciphering', action='store_true', default=False, help='Disable ciphering')
option_parser.add_argument("--rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc',
help="message check (rc=redundency check, cc=crypt. checksum, ds=digital signature)")
option_parser.add_argument('--por-in-submit', action='store_true', default=False,
help='require PoR to be sent via SMS-SUBMIT')
option_parser.add_argument('--por-no-ciphering', action='store_true', default=False, help='Disable ciphering (PoR)')
option_parser.add_argument("--por-rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc',
help="PoR check (rc=redundency check, cc=crypt. checksum, ds=digital signature)")
option_parser.add_argument("--por_req", choices=POR_REQ.decmapping.values(), default='por_required',
help="Proof of Receipt requirements")
option_parser.add_argument('--src-addr', default='12', type=str, help='SMS source address (MSISDN)')
option_parser.add_argument('--dest-addr', default='23', type=str, help='SMS destination address (MSISDN)')
option_parser.add_argument('--timeout', default=10, type=int, help='Maximum response waiting time')
option_parser.add_argument('-a', '--apdu', action='append', required=True, type=is_hexstr, help='C-APDU to send')
opts = option_parser.parse_args()
logging.basicConfig(level=logging.DEBUG if opts.verbose else logging.INFO,
format='%(asctime)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
if opts.kic_idx != opts.kid_idx:
logger.warning("KIC index (%s) and KID index (%s) are different (security violation, card should reject message)",
opts.kic_idx, opts.kid_idx)
ota_keyset = OtaKeyset(algo_crypt=opts.algo_crypt,
kic_idx=opts.kic_idx,
kic=h2b(opts.kic),
algo_auth=opts.algo_auth,
kid_idx=opts.kid_idx,
kid=h2b(opts.kid),
cntr=opts.cntr)
spi = {'counter' : opts.cntr_req,
'ciphering' : not opts.no_ciphering,
'rc_cc_ds': opts.rc_cc_ds,
'por_in_submit': opts.por_in_submit,
'por_shall_be_ciphered': not opts.por_no_ciphering,
'por_rc_cc_ds': opts.por_rc_cc_ds,
'por': opts.por_req}
apdu = h2b("".join(opts.apdu))
smpp_handler = SmppHandler(opts.host, opts.port, opts.system_id, opts.password, ota_keyset, spi, h2b(opts.tar))
resp, sw = smpp_handler.transceive_apdu(apdu, opts.src_addr, opts.dest_addr, opts.timeout)
print("%s %s" % (b2h(resp), b2h(sw)))

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env python3
# small utility program to deal with 5G SUCI key material, at least for the ECIES Protection Scheme
# Profile A (curve25519) and B (secp256r1)
# (C) 2024 by Harald Welte <laforge@osmocom.org>
# SPDX-License-Identifier: GPL-2.0+
import argparse
from osmocom.utils import b2h
from Cryptodome.PublicKey import ECC
# if used with pycryptodome < v3.21.0 you will get the following error when using curve25519:
# "Cryptodome.PublicKey.ECC.UnsupportedEccFeature: Unsupported ECC purpose (OID: 1.3.101.110)"
def gen_key(opts):
# FIXME: avoid overwriting key files
mykey = ECC.generate(curve=opts.curve)
data = mykey.export_key(format='PEM')
with open(opts.key_file, "wt") as f:
f.write(data)
def dump_pkey(opts):
#with open("curve25519-1.key", "r") as f:
with open(opts.key_file, "r") as f:
data = f.read()
mykey = ECC.import_key(data)
der = mykey.public_key().export_key(format='raw', compress=opts.compressed)
print(b2h(der))
arg_parser = argparse.ArgumentParser(description="""Generate or export SUCI keys for 5G SA networks""")
arg_parser.add_argument('--key-file', help='The key file to use', required=True)
subparsers = arg_parser.add_subparsers(dest='command', help="The command to perform", required=True)
parser_genkey = subparsers.add_parser('generate-key', help='Generate a new key pair')
parser_genkey.add_argument('--curve', help='The ECC curve to use', choices=['secp256r1','curve25519'], required=True)
parser_dump_pkey = subparsers.add_parser('dump-pub-key', help='Dump the public key')
parser_dump_pkey.add_argument('--compressed', help='Use point compression', action='store_true')
if __name__ == '__main__':
opts = arg_parser.parse_args()
if opts.command == 'generate-key':
gen_key(opts)
elif opts.command == 'dump-pub-key':
dump_pkey(opts)

View File

@@ -1,13 +1,12 @@
#!/usr/bin/env python3
# A more useful version of the 'unber' tool provided with asn1c:
# 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
from pySim.utils import bertlv_parse_one, bertlv_encode_tag, b2h, h2b
def process_one_level(content: bytes, indent: int):
remainder = content
@@ -36,8 +35,5 @@ if __name__ == '__main__':
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

@@ -4,7 +4,7 @@
# 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

View File

@@ -1,103 +0,0 @@
Guide: Installing JAVA-card applets
===================================
Almost all modern-day UICC cards have some form of JAVA-card / Sim-Toolkit support, which allows the installation
of customer specific JAVA-card applets. The installation of JAVA-card applets is usually done via the standardized
GlobalPlatform (GPC_SPE_034) ISD (Issuer Security Domain) application interface during the card provisioning process.
(it is also possible to load JAVA-card applets in field via OTA-SMS, but that is beyond the scope of this guide). In
this guide we will go through the individual steps that are required to load JAVA-card applet onto an UICC card.
Preparation
~~~~~~~~~~~
In this example we will install the CAP file HelloSTK_09122024.cap [1] on an sysmoISIM-SJA2 card. Since the interface
is standardized, the exact card model does not matter.
The example applet makes use of the STK (Sim-Toolkit), so we must supply STK installation parameters. Those
parameters are supplied in the form of a hexstring and should be provided by the applet manufacturer. The available
parameters and their exact encoding is specified in ETSI TS 102 226, section 8.2.1.3.2.1. The installation of
HelloSTK_09122024.cap [1], will require the following STK installation parameters: "010001001505000000000000000000000000"
During the installation, we also have to set a memory quota for the volatile and for the non volatile card memory.
Those values also should be provided by the applet manufacturer. In this example, we will allow 255 bytes of volatile
memory and 255 bytes of non volatile memory to be consumed by the applet.
To install JAVA-card applets, one must be in the possession of the key material belonging to the card. The keys are
usually provided by the card manufacturer. The following example will use the following keyset:
+---------+----------------------------------+
| Keyname | Keyvalue |
+=========+==================================+
| DEK/KIK | 5524F4BECFE96FB63FC29D6BAAC6058B |
+---------+----------------------------------+
| ENC/KIC | 542C37A6043679F2F9F71116418B1CD5 |
+---------+----------------------------------+
| MAC/KID | 34F11BAC8E5390B57F4E601372339E3C |
+---------+----------------------------------+
[1] https://osmocom.org/projects/cellular-infrastructure/wiki/HelloSTK
Applet Installation
~~~~~~~~~~~~~~~~~~~
To prepare the installation, a secure channel to the ISD must be established first:
::
pySIM-shell (00:MF)> select ADF.ISD
{
"application_id": "a000000003000000",
"proprietary_data": {
"maximum_length_of_data_field_in_command_message": 255
}
}
pySIM-shell (00:MF/ADF.ISD)> establish_scp02 --key-dek 5524F4BECFE96FB63FC29D6BAAC6058B --key-enc 542C37A6043679F2F9F71116418B1CD5 --key-mac 34F11BAC8E5390B57F4E601372339E3C --security-level 1
Successfully established a SCP02[01] secure channel
.. warning:: In case you get an "EXCEPTION of type 'ValueError' occurred with message: card cryptogram doesn't match" error message, it is very likely that there is a problem with the key material. The card may lock the ISD access after a certain amount of failed tries. Carefully check the key material any try again.
When the secure channel is established, we are ready to install the applet. The installation normally is a multi step
procedure, where the loading of an executable load file is announced first, then loaded and then installed in a final
step. The pySim-shell command ``install_cap`` automatically takes care of those three steps.
::
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> install_cap /home/user/HelloSTK_09122024.cap --install-parameters-non-volatile-memory-quota 255 --install-parameters-volatile-memory-quota 255 --install-parameters-stk 010001001505000000000000000000000000
loading cap file: /home/user/HelloSTK_09122024.cap ...
parameters:
security-domain-aid: a000000003000000
load-file: 569 bytes
load-file-aid: d07002ca44
module-aid: d07002ca44900101
application-aid: d07002ca44900101
install-parameters: c900ef1cc80200ffc70200ffca12010001001505000000000000000000000000
step #1: install for load...
step #2: load...
Loaded a total of 573 bytes in 3 blocks. Don't forget install_for_install (and make selectable) now!
step #3: install_for_install (and make selectable)...
done.
The applet is now installed on the card. We can now quit pySim-shell and remove the card from the reader and test the
applet in a mobile phone. There should be a new STK application with one menu entry shown, that will greet the user
when pressed.
Applet Removal
~~~~~~~~~~~~~~
To remove the applet, we must establish a secure channel to the ISD (see above). Then we can delete the applet using the
``delete_card_content`` command.
::
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> delete_card_content D07002CA44 --delete-related-objects
The parameter "D07002CA44" is the load-file-AID of the applet. The load-file-AID is encoded in the .cap file and also
displayed during the installation process. It is also important to note that when the applet is installed, it cannot
be installed (under the same AID) again until it is removed.

View File

@@ -1,342 +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.
pySim already includes two CardKeyProvider implementations. One to retrieve
key material from a CSV file (`CardKeyProviderCsv`) and a second one that allows
to retrieve the key material from a PostgreSQL database (`CardKeyProviderPgsql`).
Both implementations equally implement a column encryption scheme that allows
to protect sensitive columns using a *transport key*
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.
The `CardKeyProviderCsv` is suitable to manage small amounts of key material
locally. However, if your card inventory is very large and the key material
must be made available on multiple sites, the `CardKeyProviderPgsql` is the
better option.
The CardKeyProviderPgsql
------------------------
With the `CardKeyProviderPgsql` you can use a PostgreSQL database as storage
medium. The implementation comes with a CSV importer tool that consumes the
same CSV files you would normally use with the `CardKeyProviderCsv`, so you
can just use your existing CSV files and import them into the database.
Requirements
^^^^^^^^^^^^
The `CardKeyProviderPgsql` uses the `Psycopg` PostgreSQL database adapter
(https://www.psycopg.org). `Psycopg` is not part of the default requirements
of pySim-shell and must be installed separately. `Psycopg` is available as
Python package under the name `psycopg2-binary`.
Setting up the database
^^^^^^^^^^^^^^^^^^^^^^^
From the perspective of the database, the `CardKeyProviderPgsql` has only
minimal requirements. You do not have to create any tables in advance. An empty
database and at least one user that may create, alter and insert into tables is
sufficient. However, for increased reliability and as a protection against
incorrect operation, the `CardKeyProviderPgsql` supports a hierarchical model
with three users (or roles):
* **admin**:
This should be the owner of the database. It is intended to be used for
administrative tasks like adding new tables or adding new columns to existing
tables. This user should not be used to insert new data into tables or to access
data from within pySim-shell using the `CardKeyProviderPgsql`
* **importer**:
This user is used when feeding new data into an existing table. It should only
be able to insert new rows into existing tables. It should not be used for
administrative tasks or to access data from within pySim-shell using the
`CardKeyProviderPgsql`
* **reader**:
To access data from within pySim shell using the `CardKeyProviderPgsql` the
reader user is the correct one to use. This user should have no write access
to the database or any of the tables.
Creating a config file
^^^^^^^^^^^^^^^^^^^^^^
The default location for the config file is `~/.osmocom/pysim/card_data_pgsql.cfg`
The file uses `yaml` syntax and should look like the example below:
::
host: "127.0.0.1"
db_name: "my_database"
table_names:
- "uicc_keys"
- "euicc_keys"
db_users:
admin:
name: "my_admin_user"
pass: "my_admin_password"
importer:
name: "my_importer_user"
pass: "my_importer_password"
reader:
name: "my_reader_user"
pass: "my_reader_password"
This file is used by pySim-shell and by the importer tool. Both expect the file
in the aforementioned location. In case you want to store the file in a
different location you may use the `--pgsql` commandline option to provide a
custom config file path.
The hostname and the database name for the PostgreSQL database is set with the
`host` and `db_name` fields. The field `db_users` sets the user names and
passwords for each of the aforementioned users (or roles). In case only a single
admin user is used, all three entries may be populated with the same user name
and password (not recommended)
The field `table_names` sets the tables that the `CardKeyProviderPgsql` shall
use to query to locate card key data. You can set up as many tables as you
want, `CardKeyProviderPgsql` will query them in order, one by one until a
matching entry is found.
NOTE: In case you do not want to disclose the admin and the importer credentials
to pySim-shell you may remove those lines. pySim-shell will only require the
`reader` entry under `db_users`.
Using the Importer
^^^^^^^^^^^^^^^^^^
Before data can be imported, you must first create a database table. Tables
are created with the provided importer tool, which can be found under
`contrib/csv-to-pgsql.py`. This tool is used to create the database table and
read the data from the provided CSV file into the database.
As mentioned before, all CSV file formats that work with `CardKeyProviderCsv`
may be used. To demonstrate how the import process works, let's assume you want
to import a CSV file format that looks like the following example. Let's also
assume that you didn't get the Global Platform keys from your card vendor for
this batch of UICC cards, so your CSV file lacks the columns for those fields.
::
"id","imsi","iccid","acc","pin1","puk1","pin2","puk2","ki","opc","adm1"
"card1","999700000000001","8900000000000000001","0001","1111","11111111","0101","01010101","11111111111111111111111111111111","11111111111111111111111111111111","11111111"
"card2","999700000000002","8900000000000000002","0002","2222","22222222","0202","02020202","22222222222222222222222222222222","22222222222222222222222222222222","22222222"
"card3","999700000000003","8900000000000000003","0003","3333","22222222","0303","03030303","33333333333333333333333333333333","33333333333333333333333333333333","33333333"
Since this is your first import, the database still lacks the table. To
instruct the importer to create a new table, you may use the `--create-table`
option. You also have to pick an appropriate name for the table. Any name may
be chosen as long as it contains the string `uicc_keys` or `euicc_keys`,
depending on the type of data (`UICC` or `eUICC`) you intend to store in the
table. The creation of the table is an administrative task and can only be done
with the `admin` user. The `admin` user is selected using the `--admin` switch.
::
$ PYTHONPATH=../ ./csv-to-pgsql.py --csv ./csv-to-pgsql_example_01.csv --table-name uicc_keys --create-table --admin
INFO: CSV file: ./csv-to-pgsql_example_01.csv
INFO: CSV file columns: ['ID', 'IMSI', 'ICCID', 'ACC', 'PIN1', 'PUK1', 'PIN2', 'PUK2', 'KI', 'OPC', 'ADM1']
INFO: Using config file: /home/user/.osmocom/pysim/card_data_pgsql.cfg
INFO: Database host: 127.0.0.1
INFO: Database name: my_database
INFO: Database user: my_admin_user
INFO: New database table created: uicc_keys
INFO: Database table: uicc_keys
INFO: Database table columns: ['ICCID', 'IMSI']
INFO: Adding missing columns: ['PIN2', 'PUK1', 'PUK2', 'ACC', 'ID', 'PIN1', 'ADM1', 'KI', 'OPC']
INFO: Changes to table uicc_keys committed!
The importer has created a new table with the name `uicc_keys`. The table is
now ready to be filled with data.
::
$ PYTHONPATH=../ ./csv-to-pgsql.py --csv ./csv-to-pgsql_example_01.csv --table-name uicc_keys
INFO: CSV file: ./csv-to-pgsql_example_01.csv
INFO: CSV file columns: ['ID', 'IMSI', 'ICCID', 'ACC', 'PIN1', 'PUK1', 'PIN2', 'PUK2', 'KI', 'OPC', 'ADM1']
INFO: Using config file: /home/user/.osmocom/pysim/card_data_pgsql.cfg
INFO: Database host: 127.0.0.1
INFO: Database name: my_database
INFO: Database user: my_importer_user
INFO: Database table: uicc_keys
INFO: Database table columns: ['ICCID', 'IMSI', 'PIN2', 'PUK1', 'PUK2', 'ACC', 'ID', 'PIN1', 'ADM1', 'KI', 'OPC']
INFO: CSV file import done, 3 rows imported
INFO: Changes to table uicc_keys committed!
A quick `SELECT * FROM uicc_keys;` at the PostgreSQL console should now display
the contents of the CSV file you have fed into the importer.
Let's now assume that with your next batch of UICC cards your vendor includes
the Global Platform keys so your CSV format changes. It may now look like this:
::
"id","imsi","iccid","acc","pin1","puk1","pin2","puk2","ki","opc","adm1","scp02_dek_1","scp02_enc_1","scp02_mac_1"
"card4","999700000000004","8900000000000000004","0004","4444","44444444","0404","04040404","44444444444444444444444444444444","44444444444444444444444444444444","44444444","44444444444444444444444444444444","44444444444444444444444444444444","44444444444444444444444444444444"
"card5","999700000000005","8900000000000000005","0005","4444","55555555","0505","05050505","55555555555555555555555555555555","55555555555555555555555555555555","55555555","55555555555555555555555555555555","55555555555555555555555555555555","55555555555555555555555555555555"
"card6","999700000000006","8900000000000000006","0006","4444","66666666","0606","06060606","66666666666666666666666666666666","66666666666666666666666666666666","66666666","66666666666666666666666666666666","66666666666666666666666666666666","66666666666666666666666666666666"
When importing data from an updated CSV format the database table also has
to be updated. This is done using the `--update-columns` switch. Like when
creating new tables, this operation also requires admin privileges, so the
`--admin` switch is required again.
::
$ PYTHONPATH=../ ./csv-to-pgsql.py --csv ./csv-to-pgsql_example_02.csv --table-name uicc_keys --update-columns --admin
INFO: CSV file: ./csv-to-pgsql_example_02.csv
INFO: CSV file columns: ['ID', 'IMSI', 'ICCID', 'ACC', 'PIN1', 'PUK1', 'PIN2', 'PUK2', 'KI', 'OPC', 'ADM1', 'SCP02_DEK_1', 'SCP02_ENC_1', 'SCP02_MAC_1']
INFO: Using config file: /home/user/.osmocom/pysim/card_data_pgsql.cfg
INFO: Database host: 127.0.0.1
INFO: Database name: my_database
INFO: Database user: my_admin_user
INFO: Database table: uicc_keys
INFO: Database table columns: ['ICCID', 'IMSI', 'PIN2', 'PUK1', 'PUK2', 'ACC', 'ID', 'PIN1', 'ADM1', 'KI', 'OPC']
INFO: Adding missing columns: ['SCP02_ENC_1', 'SCP02_MAC_1', 'SCP02_DEK_1']
INFO: Changes to table uicc_keys committed!
When the new table columns are added, the import may be continued like the
first one:
::
$ PYTHONPATH=../ ./csv-to-pgsql.py --csv ./csv-to-pgsql_example_02.csv --table-name uicc_keys
INFO: CSV file: ./csv-to-pgsql_example_02.csv
INFO: CSV file columns: ['ID', 'IMSI', 'ICCID', 'ACC', 'PIN1', 'PUK1', 'PIN2', 'PUK2', 'KI', 'OPC', 'ADM1', 'SCP02_DEK_1', 'SCP02_ENC_1', 'SCP02_MAC_1']
INFO: Using config file: /home/user/.osmocom/pysim/card_data_pgsql.cfg
INFO: Database host: 127.0.0.1
INFO: Database name: my_database
INFO: Database user: my_importer_user
INFO: Database table: uicc_keys
INFO: Database table columns: ['ICCID', 'IMSI', 'PIN2', 'PUK1', 'PUK2', 'ACC', 'ID', 'PIN1', 'ADM1', 'KI', 'OPC', 'SCP02_ENC_1', 'SCP02_MAC_1', 'SCP02_DEK_1']
INFO: CSV file import done, 3 rows imported
INFO: Changes to table uicc_keys committed!
On the PostgreSQL console a `SELECT * FROM uicc_keys;` should now show the
imported data with the added columns. All important data should now also be
available from within pySim-shell via the `CardKeyProviderPgsql`.
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 (or
database).
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`
NOTE: When using `CardKeyProviderPqsl`, the input CSV files must be encrypted
before import.
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 CardKeyProvider finds a line (row) in your CSV file
(or database) 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,17 +18,9 @@ sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = 'osmopysim-usermanual'
copyright = '2009-2025 by Sylvain Munaut, Harald Welte, Philipp Maier, Supreeth Herle, Merlin Chlosta'
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'
# PDF: Avoid that the authors list exceeds the page by inserting '\and'
# manually as line break (https://github.com/sphinx-doc/sphinx/issues/6875)
latex_elements = {
"maketitle":
r"""\author{Sylvain Munaut, Harald Welte, Philipp Maier, \and Supreeth Herle, Merlin Chlosta}
\sphinxmaketitle
"""
}
# -- General configuration ---------------------------------------------------

View File

@@ -41,13 +41,8 @@ pySim consists of several parts:
shell
trace
legacy
smpp2sim
library
library-esim
osmo-smdpp
sim-rest
suci-keytool
saip-tool
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 managed 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 with 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:

View File

@@ -1,95 +0,0 @@
pySim eSIM libraries
====================
The pySim eSIM libraries implement a variety of functionality related to the GSMA eSIM universe,
including the various interfaces of SGP.21 + SGP.22, as well as Interoperable Profile decioding,
validation, personalization and encoding.
.. automodule:: pySim.esim
:members:
GSMA SGP.21/22 Remote SIM Provisioning (RSP) - High Level
---------------------------------------------------------
pySim.esim.rsp
~~~~~~~~~~~~~~
.. automodule:: pySim.esim.rsp
:members:
pySim.esim.es2p
~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.es2p
:members:
pySim.esim.es8p
~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.es8p
:members:
pySim.esim.es9p
~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.es9p
:members:
GSMA SGP.21/22 Remote SIM Provisioning (RSP) - Low Level
--------------------------------------------------------
pySim.esim.bsp
~~~~~~~~~~~~~~
.. automodule:: pySim.esim.bsp
:members:
pySim.esim.http_json_api
~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.http_json_api
:members:
pySim.esim.x509_cert
~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.x509_cert
:members:
SIMalliance / TCA Interoperable Profile
---------------------------------------
pySim.esim.saip
~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip
:members:
pySim.esim.saip.oid
~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip.oid
:members:
pySim.esim.saip.personalization
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip.personalization
:members:
pySim.esim.saip.templates
~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip.templates
:members:
pySim.esim.saip.validation
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: pySim.esim.saip.validation
:members:

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

@@ -19,42 +19,32 @@ 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 matching eUICCs.
* uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your osmo-smdppp
would be running at the host name `testsmdpplus1.example.com`
* doesn't understand profile state. Any profile can always be downloaded any number of times, irrespective
of the EID or whether it was downloaded 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)
of the EID or whether it was donwloaded before
* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical
* **is absolutely insecure**, as it
* does not perform 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
* does not perform any certificate verification
* does not evaluate/consider any *Matching ID* or *Confirmation Code*
* stores the sessions in an unencrypted _python shelve_ and is hence leaking one-time key materials
used for profile encryption and signing.
Running osmo-smdpp
------------------
osmo-smdpp comes with built-in TLS support which is enabled by default. However, it is always possible to
disable the built-in TLS support if needed.
In order to use osmo-smdpp without the built-in TLS support, it has to be put behind a TLS reverse proxy,
which terminates the ES9+ HTTPS traffic from the LPA, and then forwards it as plain HTTP to osmo-smdpp.
NOTE: The built in TLS support in osmo-smdpp makes use of the python *twisted* framework. Older versions
of this framework appear to have problems when using the example elliptic curve certificates (both NIST and
Brainpool) from GSMA.
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 chose to use `nginx` as TLS reverse proxy, you can use the following configuration snippet::
If you use `nginx` as web server, you can use the following configuration snippet::
upstream smdpp {
server localhost:8000;
@@ -84,56 +74,31 @@ If you chose to use `nginx` as TLS reverse proxy, you can use the following conf
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*.
osmo-smdpp
~~~~~~~~~~
osmo-smdpp currently doesn't have any configuration file or command line options. You just run it,
and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
The `smdpp-data/certs`` directory contains the DPtls, DPauth and DPpb as well as CI certificates
used; they are copied from GSMA SGP.26 v2.
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used. The file names (without
.der suffix) are looked up by the matchingID parameter from the activation code presented by the LPA.
commandline options
~~~~~~~~~~~~~~~~~~~
Typically, you just run osmo-smdpp without any arguments, and it will bind its built-in HTTPS ES9+ interface to
`localhost` TCP port 443. In this case an external TLS reverse proxy is not needed.
osmo-smdpp currently doesn't have any configuration file.
There are command line options for binding:
Bind the HTTPS ES9+ to a port other than 443::
./osmo-smdpp.py -p 8443
Disable the built-in TLS support and bind the plain-HTTP ES9+ to a port 8000::
./osmo-smdpp.py -p 8000 --nossl
Bind the HTTP ES9+ to a different local interface::
./osmo-smdpp.py -H 127.0.0.2
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. In case osmo-smdpp is used with built-in TLS support,
it will use the certificates provided in smdpp-data.
NOTE: The HTTPS ES9+ interface cannot be addressed by the LPA directly via its IP address. The reason for this is that
the included SGP.26 (DPtls) test certificates explicitly restrict the hostname to `testsmdpplus1.example.com` in the
`X509v3 Subject Alternative Name` extension. Using a bare IP address as hostname may cause the certificate to be
rejected by the LPA.
It must also accept the TLS certificates used by your TLS proxy.
Supported eUICC
~~~~~~~~~~~~~~~
If you run osmo-smdpp with the included SGP.26 (DPauth, DPpb) certificates, you must use an eUICC with matching SGP.26
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.

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

View File

@@ -1,137 +0,0 @@
saip-tool
=========
eSIM profiles are stored as a sequence of profile element (PE) objects in an ASN.1 DER encoded binary file. To inspect,
verify or make changes to those files, the `saip-tool.py` utility can be used.
NOTE: The file format, eSIM SAIP (SimAlliance Interoperable Profile) is specified in `TCA eUICC Profile Package:
Interoperable Format Technical Specification`
Profile Package Examples
~~~~~~~~~~~~~~~~~~~~~~~~
pySim ships with a set of TS48 profile package examples. Those examples can be found in `pysim/smdpp-data/upp`. The
files can be used as input for `saip-tool.py`. (see also GSMA TS.48 - Generic eUICC Test Profile for Device Testing)
See also: https://github.com/GSMATerminals/Generic-eUICC-Test-Profile-for-Device-Testing-Public
JAVA card applets
~~~~~~~~~~~~~~~~~
The `saip-tool.py` can also be used to manage JAVA-card applets (Application PE) inside a profile package. The user has
the option to add, remove and inspect applications and their instances. In the following we will discuss a few JAVA-card
related use-cases of `saip-tool.py`
NOTE: see also `contrib` folder for script examples (`saip-tool_example_*.sh`)
Inserting applications
----------------------
An application is usually inserted in two steps. In the first step, the application PE is created and populated with
the executable code from a provided `.cap` or `.ijc` file. The user also has to pick a suitable load block AID.
The application instance, which exists inside the application PE, is created in a second step. Here the user must
reference the load block AID and pick, among other application related parameters, a suitable class and instance AID.
Example: Adding a JAVA-card applet to an existing profile package
::
# Step #1: Create the application PE and load the ijc contents from the .cap file:
$ ./contrib/saip-tool.py upp.der add-app --output-file upp_with_app.der --applet-file app.cap --aid '1122334455'
Read 28 PEs from file 'upp.der'
Applying applet file: 'app.cap'...
application PE inserted into PE Sequence after securityDomain PE AID: a000000151000000
Writing 29 PEs to file 'upp_with_app.der'...
# Step #2: Create the application instance inside the application PE created in step #1:
$ ./contrib/saip-tool.py upp_with_app.der add-app-inst --output-file upp_with_app_and_instance.der \
--aid '1122334455' \
--class-aid '112233445501' \
--inst-aid '112233445501' \
--app-privileges '00' \
--app-spec-pars '00' \
--uicc-toolkit-app-spec-pars '01001505000000000000000000000000'
Read 29 PEs from file 'upp_with_app.der'
Found Load Package AID: 1122334455, adding new instance AID: 112233445501 to Application PE...
Writing 29 PEs to file 'upp_with_app_and_instance.der'...
NOTE: The parameters of the sub-commands `add-app` and `add-app-inst` are application specific. It is up to the application
developer to pick parameters that suit the application correctly. For an exact command reference see section
`saip-tool syntax`. For parameter details see `TCA eUICC Profile Package: Interoperable Format Technical Specification`,
section 8.7 and ETSI TS 102 226, section 8.2.1.3.2
Inspecting applications
-----------------------
To inspect the application PE contents of an existing profile package, sub-command `info` with parameter '--apps' can
be used. This command lists out all application and their parameters in detail. This allows an application developer
to check if the applet insertaion was carried out as expected.
Example: Listing applications and their parameters
::
$ ./contrib/saip-tool.py upp_with_app_and_instance.der info --apps
Read 29 PEs from file 'upp_with_app_and_instance.der'
Application #0:
loadBlock:
loadPackageAID: '1122334455' (5 bytes)
loadBlockObject: '01000fdecaffed010204000105d07002ca440200...681080056810a00633b44104b431066800a10231' (569 bytes)
instanceList[0]:
applicationLoadPackageAID: '1122334455' (5 bytes)
classAID: '112233445501' (8 bytes)
instanceAID: '112233445501' (8 bytes)
applicationPrivileges: '00' (1 bytes)
lifeCycleState: '07' (1 bytes)
applicationSpecificParametersC9: '00' (1 bytes)
applicationParameters:
uiccToolkitApplicationSpecificParametersField: '01001505000000000000000000000000' (16 bytes)
In case further analysis with external tools or transfer of applications from one profile package to another is
necessary, the executable code in the `loadBlockObject` field can be extracted to an `.ijc` or an `.cap` file.
Example: Extracting applications from a profile package
::
$ ./contrib/saip-tool.py upp_with_app_and_instance.der extract-apps --output-dir ./apps --format ijc
Read 29 PEs from file 'upp_with_app_and_instance.der'
Writing Load Package AID: 1122334455 to file ./apps/8949449999999990023f-1122334455.ijc
Removing applications
---------------------
An application PE can be removed using sub-command `remove-app`. The user passes the load package AID as parameter. Then
`saip-tool.py` will search for the related application PE and delete it from the PE sequence.
Example: Remove an application from a profile package
::
$ ./contrib/saip-tool.py upp_with_app_and_instance.der remove-app --output-file upp_without_app.der --aid '1122334455'
Read 29 PEs from file 'upp_with_app_and_instance.der'
Found Load Package AID: 1122334455, removing related PE (id=23) from Sequence...
Removing PE application (id=23) from Sequence...
Writing 28 PEs to file 'upp_without_app.der'...
In some cases it is useful to remove only an instance from an existing application PE. This may be the case when the
an application developer wants to modify parameters of an application by removing and re-adding the instance. The
operation basically rolls the state back to step 1 explained in section :ref:`Inserting applications`
Example: Remove an application instance from an application PE
::
$ ./contrib/saip-tool.py upp_with_app_and_instance.der remove-app-inst --output-file upp_without_app.der --aid '1122334455' --inst-aid '112233445501'
Read 29 PEs from file 'upp_with_app_and_instance.der'
Found Load Package AID: 1122334455, removing instance AID: 112233445501 from Application PE...
Removing instance from Application PE...
Writing 29 PEs to file 'upp_with_app.der'...
saip-tool syntax
~~~~~~~~~~~~~~~~
.. argparse::
:module: contrib.saip-tool
:func: parser
:prog: contrib/saip-tool.py

View File

@@ -1,4 +1,4 @@
pySim-shell
pySim-shell
===========
pySim-shell is an interactive command line shell for all kind of interactions with SIM cards,
@@ -20,9 +20,6 @@ The pySim-shell interactive shell provides commands for
* if your card supports it, and you have the related privileges: resizing, creating, enabling and disabling of
files
* performing GlobalPlatform operations, including establishment of Secure Channel Protocol (SCP), Installing
applications, installing key material, etc.
* listing/enabling/disabling/deleting eSIM profiles on Consumer eUICC
By means of using the python ``cmd2`` module, various useful features improve usability:
@@ -67,18 +64,8 @@ Usage Examples
:caption: Tutorials for pySIM-shell:
suci-tutorial
cap-tutorial
Advanced Topics
---------------
.. toctree::
:maxdepth: 1
:caption: Advanced pySIM-shell topics
card-key-provider
remote-access
cmd2 basics
-----------
@@ -146,32 +133,6 @@ optional files in some later 3GPP release) were not found on the card, or were i
trying to SELECT them.
fsdump
~~~~~~
.. argparse::
:module: pySim-shell
:func: PySimCommands.fsdump_parser
Please note that `fsdump` works relative to the current working
directory, so if you are in `MF`, then the dump will contain all known
files on the card. However, if you are in `ADF.ISIM`, only files below
that ADF will be part of the dump.
Furthermore, it is strongly advised to first enter the ADM1 pin
(`verify_adm`) to maximize the chance of having permission to read
all/most files.
One use case for this is to systematically analyze the differences between the contents of two
cards. To do this, you can create fsdumps of the two cards, and then use some general-purpose JSON
diffing tool like `jycm --show` (see https://github.com/eggachecat/jycm).
Example:
::
pySIM-shell (00:MF)> fsdump > /tmp/fsdump.json
pySIM-shell (00:MF)>
tree
~~~~
Display a tree of the card filesystem. It is important to note that this displays a tree
@@ -512,18 +473,7 @@ sequence including the electrical power down.
:module: pySim.ts_102_221
:func: CardProfileUICC.AddlShellCommands.resume_uicc_parser
terminal_capability
~~~~~~~~~~~~~~~~~~~
This command allows you to perform the TERMINAL CAPABILITY command towards the card.
TS 102 221 specifies the TERMINAL CAPABILITY command using which the
terminal (Software + hardware talking to the card) can expose their
capabilities. This is also used in the eUICC universe to let the eUICC
know which features are supported.
.. argparse::
:module: pySim.ts_102_221
:func: CardProfileUICC.AddlShellCommands.term_cap_parser
Linear Fixed EF commands
@@ -544,9 +494,6 @@ read_record_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.read_rec_dec_parser
If this command fails, it means that the record is not decodable, and you should use the :ref:`read_record`
command and proceed with manual decoding of the contents.
read_records
~~~~~~~~~~~~
@@ -561,9 +508,6 @@ read_records_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.read_recs_dec_parser
If this command fails, it means that the record[s] are not decodable, and you should use the :ref:`read_records`
command and proceed with manual decoding of the contents.
update_record
~~~~~~~~~~~~~
@@ -578,9 +522,6 @@ update_record_decoded
:module: pySim.filesystem
:func: LinFixedEF.ShellCommands.upd_rec_dec_parser
If this command fails, it means that the record is not encodable; please check your input and/or use the raw
:ref:`update_record` command.
edit_record_decoded
~~~~~~~~~~~~~~~~~~~
@@ -599,12 +540,6 @@ back to the record on the SIM card.
This allows for easy interactive modification of records.
If this command fails before the editor is spawned, it means that the current record contents is not decodable,
and you should use the :ref:`update_record_decoded` or :ref:`update_record` command.
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
encodable; please check your input and/or us the raw :ref:`update_record` comamdn.
decode_hex
~~~~~~~~~~
@@ -633,8 +568,6 @@ read_binary_decoded
:module: pySim.filesystem
:func: TransparentEF.ShellCommands.read_bin_dec_parser
If this command fails, it means that the file is not decodable, and you should use the :ref:`read_binary`
command and proceed with manual decoding of the contents.
update_binary
~~~~~~~~~~~~~
@@ -688,10 +621,6 @@ The below example demonstrates this by modifying the ciphering indicator field w
"extensions": "ff"
}
If this command fails, it means that the file is not encodable; please check your input and/or use the raw
:ref:`update_binary` command.
edit_binary_decoded
~~~~~~~~~~~~~~~~~~~
This command will read the selected binary EF, decode it to its JSON representation, save
@@ -705,12 +634,6 @@ to the SIM card.
This allows for easy interactive modification of file contents.
If this command fails before the editor is spawned, it means that the current file contents is not decodable,
and you should use the :ref:`update_binary_decoded` or :ref:`update_binary` command.
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
encodable; please check your input and/or us the raw :ref:`update_binary` comamdn.
decode_hex
~~~~~~~~~~
@@ -1003,25 +926,7 @@ aram_delete_all
~~~~~~~~~~~~~~~
This command will request deletion of all access rules stored within the
ARA-M applet. Use it with caution, there is no undo. Any rules later
intended must be manually inserted again using :ref:`aram_store_ref_ar_do`
aram_lock
~~~~~~~~~
This command allows to lock the access to the STORE DATA command. This renders
all access rules stored within the ARA-M applet effectively read-only. The lock
can only be removed via a secure channel to the security domain and is therefore
suitable to prevent unauthorized changes to ARA-M rules.
Removal of the lock:
::
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> install_for_personalization A00000015141434C00
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> apdu --expect-sw 9000 80E2900001A2
NOTE: ARA-M Locking is a proprietary feature that is specific to sysmocom's
fork of Bertrand Martel's ARA-M implementation. ARA-M Locking is supported in
newer (2025) applet versions from v0.1.0 onward.
intended must be manually inserted again using `aram_store_ref_ar_do`
GlobalPlatform commands
@@ -1066,35 +971,11 @@ delete_key
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.del_key_parser
load
~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.load_parser
install_cap
~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.install_cap_parser
install_for_personalization
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.inst_perso_parser
install_for_install
~~~~~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.inst_inst_parser
install_for_load
~~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.global_platform
:func: ADF_SD.AddlShellCommands.inst_load_parser
:func: ADF_SD.AddlShellCommands.inst_for_perso_parser
delete_card_content
~~~~~~~~~~~~~~~~~~~
@@ -1155,7 +1036,7 @@ es10x_store_data
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.es10x_store_data_parser
:func: ADF_ISDR.AddlShellCommands.es10x_store_data_parser
get_euicc_configured_addresses
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1174,7 +1055,7 @@ set_default_dp_address
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.set_def_dp_addr_parser
:func: ADF_ISDR.AddlShellCommands.set_def_dp_addr_parser
get_euicc_challenge
~~~~~~~~~~~~~~~~~~~
@@ -1317,7 +1198,7 @@ remove_notification_from_list
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.rem_notif_parser
:func: ADF_ISDR.AddlShellCommands.rem_notif_parser
Example::
@@ -1366,7 +1247,7 @@ enable_profile
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.en_prof_parser
:func: ADF_ISDR.AddlShellCommands.en_prof_parser
Example (successful)::
@@ -1388,7 +1269,7 @@ disable_profile
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.dis_prof_parser
:func: ADF_ISDR.AddlShellCommands.dis_prof_parser
Example (successful)::
@@ -1402,7 +1283,7 @@ delete_profile
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.del_prof_parser
:func: ADF_ISDR.AddlShellCommands.del_prof_parser
Example::
@@ -1411,13 +1292,6 @@ Example::
"delete_result": "ok"
}
euicc_memory_reset
~~~~~~~~~~~~~~~~~~
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.mem_res_parser
get_eid
~~~~~~~
@@ -1436,7 +1310,7 @@ set_nickname
.. argparse::
:module: pySim.euicc
:func: CardApplicationISDR.AddlShellCommands.set_nickname_parser
:func: ADF_ISDR.AddlShellCommands.set_nickname_parser
Example::

View File

@@ -1,118 +0,0 @@
sim-rest-server
===============
Sometimes there are use cases where a [remote] application will need
access to a USIM for authentication purposes. This is, for example, in
case an IMS test client needs to perform USIM based authentication
against an IMS core.
The pysim repository contains two programs: `sim-rest-server.py` and
`sim-rest-client.py` that implement a simple approach to achieve the
above:
`sim-rest-server.py` speaks to a [usually local] USIM via the PC/SC
API and provides a high-level REST API towards [local or remote]
applications that wish to perform UMTS AKA using the USIM.
`sim-rest-client.py` implements a small example client program to
illustrate how the REST API provided by `sim-rest-server.py` can be
used.
REST API Calls
--------------
POST /sim-auth-api/v1/slot/SLOT_NR
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
where SLOT_NR is the integer-encoded slot number (corresponds to PC/SC
reader number). When using a single sysmoOCTSIM board, this is in the range of 0..7
Example: `/sim-auth-api/v1/slot/0` for the first slot.
Request Body
############
The request body is a JSON document, comprising of
1. the RAND and AUTN parameters as hex-encoded string
2. the application against which to authenticate (USIM, ISIM)
Example:
::
{
"rand": "bb685a4b2fc4d697b9d6a129dd09a091",
"autn": "eea7906f8210000004faf4a7df279b56"
}
HTTP Status Codes
#################
HTTP status codes are used to represent errors within the REST server
and the SIM reader hardware. They are not used to communicate protocol
level errors reported by the SIM Card. An unsuccessful authentication
will hence have a `200 OK` HTTP Status code and then encode the SIM
specific error information in the Response Body.
====== =========== ================================
Status Code Description
------ ----------- --------------------------------
200 OK Successful execution
400 Bad Request Request body is malformed
404 Not Found Specified SIM Slot doesn't exist
410 Gone No SIM card inserted in slot
====== =========== ================================
Response Body
#############
The response body is a JSON document, either
#. a successful outcome; encoding RES, CK, IK as hex-encoded string
#. a sync failure; encoding AUTS as hex-encoded string
#. errors
#. authentication error (incorrect MAC)
#. authentication error (security context not supported)
#. key freshness failure
#. unspecified card error
Example (success):
::
{
"successful_3g_authentication": {
"res": "b15379540ec93985",
"ck": "713fde72c28cbd282a4cd4565f3d6381",
"ik": "2e641727c95781f1020d319a0594f31a",
"kc": "771a2c995172ac42"
}
}
Example (re-sync case):
::
{
"synchronisation_failure": {
"auts": "dc2a591fe072c92d7c46ecfe97e5"
}
}
Concrete example using the included sysmoISIM-SJA2
--------------------------------------------------
This was tested using SIMs ending in IMSI numbers 45890...45899
The following command were executed successfully:
Slot 0
::
$ /usr/local/src/pysim/contrib/sim-rest-client.py -c 1 -n 0 -k 841EAD87BC9D974ECA1C167409357601 -o 3211CACDD64F51C3FD3013ECD9A582A0
-> {'rand': 'fb195c7873b20affa278887920b9dd57', 'autn': 'd420895a6aa2000089cd016f8d8ae67c'}
<- {'successful_3g_authentication': {'res': '131004db2ff1ce8e', 'ck': 'd42eb5aa085307903271b2422b698bad', 'ik': '485f81e6fd957fe3cad374adf12fe1ca', 'kc': '64d3f2a32f801214'}}
Slot 1
::
$ /usr/local/src/pysim/contrib/sim-rest-client.py -c 1 -n 1 -k 5C2CE9633FF9B502B519A4EACD16D9DF -o 9834D619E71A02CD76F00CC7AA34FB32
-> {'rand': '433dc5553db95588f1d8b93870930b66', 'autn': '126bafdcbe9e00000026a208da61075d'}
<- {'successful_3g_authentication': {'res': '026d7ac42d379207', 'ck': '83a90ba331f47a95c27a550b174c4a1f', 'ik': '31e1d10329ffaf0ca1684a1bf0b0a14a', 'kc': 'd15ac5b0fff73ecc'}}

View File

@@ -1,57 +0,0 @@
pySim-smpp2sim
==============
This is a program to emulate the entire communication path SMSC-CN-RAN-ME
that is usually between an OTA backend and the SIM card. This allows
to play with SIM OTA technology without using a mobile network or even
a mobile phone.
An external application can act as SMPP ESME and must encode (and
encrypt/sign) the OTA SMS and submit them via SMPP to this program, just
like it would submit it normally to a SMSC (SMS Service Centre). The
program then re-formats the SMPP-SUBMIT into a SMS DELIVER TPDU and
passes it via an ENVELOPE APDU to the SIM card that is locally inserted
into a smart card reader.
The path from SIM to external OTA application works the opposite way.
The default SMPP system_id is `test`. Likewise, the default SMPP
password is `test`
Running pySim-smpp2sim
----------------------
The command accepts the same command line arguments for smart card interface device selection as pySim-shell,
as well as a few SMPP specific arguments:
.. argparse::
:module: pySim-smpp2sim
:func: option_parser
:prog: pySim-smpp2sim.py
Example execution with sample output
------------------------------------
So for a simple system with a single PC/SC device, you would typically use something like
`./pySim-smpp2sim.py -p0` to start the program. You will see output like this at start-up
::
Using reader PCSC[HID Global OMNIKEY 3x21 Smart Card Reader [OMNIKEY 3x21 Smart Card Reader] 00 00]
INFO root: Binding Virtual SMSC to TCP Port 2775 at ::
The application has hence bound to local TCP port 2775 and expects your SMS-sending applications to send their
SMS there. Once you do, you will see log output like below:
::
WARNING smpp.twisted.protocol: SMPP connection established from ::ffff:127.0.0.1 to port 2775
INFO smpp.twisted.server: Added CommandId.bind_transceiver bind for 'test'. Active binds: CommandId.bind_transceiver: 1, CommandId.bind_transmitter: 0, CommandId.bind_receiver: 0. Max binds: 2
INFO smpp.twisted.protocol: Bind request succeeded for test. 1 active binds
And once your external program is sending SMS to the simulated SMSC, it will log something like
::
INFO root: SMS_DELIVER(MTI=0, MMS=False, LP=False, RP=False, UDHI=True, SRI=False, OA=AddressField(TON=international, NPI=unknown, 12), PID=7f, DCS=f6, SCTS=bytearray(b'"pR\x00\x00\x00\x00'), UDL=45, UD=b"\x02p\x00\x00(\x15\x16\x19\x12\x12\xb0\x00\x01'\xfa(\xa5\xba\xc6\x9d<^\x9d\xf2\xc7\x15]\xfd\xdeD\x9c\x82k#b\x15Ve0x{0\xe8\xbe]")
SMSPPDownload(DeviceIdentities({'source_dev_id': 'network', 'dest_dev_id': 'uicc'}),Address({'ton_npi': 0, 'call_number': '0123456'}),SMS_TPDU({'tpdu': '400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d'}))
INFO root: ENVELOPE: d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
INFO root: SW 9000: 027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c

View File

@@ -1,58 +0,0 @@
suci-keytool
============
Subscriber concealment is an important feature of the 5G SA architecture: It avoids the many privacy
issues associated with having a permanent identifier (SUPI, traditionally the IMSI) transmitted in plain text
over the air interface. Using SUCI solves this issue not just for the air interface; it even ensures the SUPI/IMSI
is not known to the visited network (VPLMN) at all.
In principle, the SUCI mechanism works by encrypting the SUPI by asymmetric (public key) cryptography:
Only the HPLMN is in possession of the private key and hence can decrypt the SUCI to the SUPI, while
each subscriber has the public key in order to encrypt their SUPI into the SUCI. In reality, the
details are more complex, as there are ephemeral keys and cryptographic MAC involved.
In any case, in order to operate a SUCI-enabled 5G SA network, you will have to
#. generate a ECC key pair of public + private key
#. deploy the public key on your USIMs
#. deploy the private key on your 5GC, specifically the UDM function
pysim contains (in its `contrib` directory) a small utility program that can make it easy to generate
such keys: `suci-keytool.py`
Generating keys
~~~~~~~~~~~~~~~
Example: Generating a *secp256r1* ECC public key pair and storing it to `/tmp/suci.key`:
::
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key generate-key --curve secp256r1
Dumping public keys
~~~~~~~~~~~~~~~~~~~
In order to store the key to SIM cards as part of `ADF.USIM/DF.5GS/EF.SUCI_Calc_Info`, you will need
a hexadecimal representation of the public key. You can achieve that using the `dump-pub-key` operation
of suci-keytool:
Example: Dumping the public key part from a previously generated key file:
::
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key dump-pub-key
0473152f32523725f5175d255da2bd909de97b1d06449a9277bc629fe42112f8643e6b69aa6dce6c86714ccbe6f2e0f4f4898d102e2b3f0c18ce26626f052539bb
If you want the point-compressed representation, you can use the `--compressed` option:
::
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key dump-pub-key --compressed
0373152f32523725f5175d255da2bd909de97b1d06449a9277bc629fe42112f864
suci-keytool syntax
~~~~~~~~~~~~~~~~~~~
.. argparse::
:module: contrib.suci-keytool
:func: arg_parser
:prog: contrib/suci-keytool.py

View File

@@ -1,56 +1,40 @@
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 *in the UE*, using 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.
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 cards (or successor products).
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).
In short, you can enable SUCI with these steps:
This document describes both methods.
* activate USIM **Service 124**
* make sure USIM **Service 125** is disabled
* store the public keys in **SUCI_Calc_Info**
* set the **Routing Indicator** (required)
If you want to disable the feature, you can just disable USIM Service 124 (and 125).
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>`__
* USIM files and structure: `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) `TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.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 current
sysmoISIM-SJA5 product
* the `sysmoISIM-SJA2 User Manual <https://sysmocom.de/manuals/sysmousim-manual.pdf>`__ for the older
sysmoISIM-SJA2 product
For specific information on sysmocom SIM cards, refer to Section 9.1 of the `sysmoUSIM User
Manual <https://www.sysmocom.de/manuals/sysmousim-manual.pdf>`__.
--------------
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
---------
@@ -99,8 +83,8 @@ By default, the file is present but empty:
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
The following JSON config defines the testfile from `TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__ Section 4.9.4 with
test keys from `TS 33.501 <hhttps://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.pdf>`__ 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``.
@@ -113,7 +97,7 @@ with ``hnet_pubkey_identifier: 27``.
{"priority": 2, "identifier": 0, "key_index": 0}],
"hnet_pubkey_list": [
{"hnet_pubkey_identifier": 27,
"hnet_pubkey": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"},
"hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"},
{"hnet_pubkey_identifier": 30,
"hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]
}
@@ -122,7 +106,7 @@ 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"}]}'
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> update_binary_decoded '{ "prot_scheme_id_list": [ {"priority": 0, "identifier": 2, "key_index": 1}, {"priority": 1, "identifier": 1, "key_index": 2}, {"priority": 2, "identifier": 0, "key_index": 0}], "hnet_pubkey_list": [ {"hnet_pubkey_identifier": 27, "hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"}, {"hnet_pubkey_identifier": 30, "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]}'
WARNING: These are TEST KEYS with publicly known/specified private keys, and hence unsafe for live/secure
deployments! For use in production networks, you need to generate your own set[s] of keys.
@@ -166,7 +150,7 @@ First, check out the USIM Service Table (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
.. list-table:: From TS31.102
:widths: 15 40
:header-rows: 1
@@ -200,7 +184,7 @@ be disabled.
USIM Error with 5G and sysmoISIM
--------------------------------
sysmoISIM-SJA2 come 5GS-enabled. By default however, the configuration stored
sysmoISIMs 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).
@@ -209,62 +193,3 @@ 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

@@ -17,124 +17,27 @@
# 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/>.
# asn1tools issue https://github.com/eerimoq/asn1tools/issues/194
# must be first here
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
import asn1tools.codecs.ber
import asn1tools.codecs.der
# do not move the code
def fix_asn1_oid_decoding():
fix_asn1_schema = """
TestModule DEFINITIONS ::= BEGIN
TestOid ::= SEQUENCE {
oid OBJECT IDENTIFIER
}
END
"""
fix_asn1_asn1 = asn1tools.compile_string(fix_asn1_schema, codec='der')
fix_asn1_oid_string = '2.999.10'
fix_asn1_encoded = fix_asn1_asn1.encode('TestOid', {'oid': fix_asn1_oid_string})
fix_asn1_decoded = fix_asn1_asn1.decode('TestOid', fix_asn1_encoded)
from pySim.utils import h2b, b2h, swap_nibbles
if (fix_asn1_decoded['oid'] != fix_asn1_oid_string):
# ASN.1 OBJECT IDENTIFIER Decoding Issue:
#
# In ASN.1 BER/DER encoding, the first two arcs of an OBJECT IDENTIFIER are
# combined into a single value: (40 * arc0) + arc1. This is encoded as a base-128
# variable-length quantity (and commonly known as VLQ or base-128 encoding)
# as specified in ITU-T X.690 §8.19, it can span multiple bytes if
# the value is large.
#
# For arc0 = 0 or 1, arc1 must be in [0, 39]. For arc0 = 2, arc1 can be any non-negative integer.
# All subsequent arcs (arc2, arc3, ...) are each encoded as a separate base-128 VLQ.
#
# The decoding bug occurs when the decoder does not properly split the first
# subidentifier for arc0 = 2 and arc1 >= 40. Instead of decoding:
# - arc0 = 2
# - arc1 = (first_subidentifier - 80)
# it may incorrectly interpret the first_subidentifier as arc0 = (first_subidentifier // 40),
# arc1 = (first_subidentifier % 40), which is only valid for arc1 < 40.
#
# This patch handles it properly for all valid OBJECT IDENTIFIERs
# with large second arcs, by applying the ASN.1 rules:
# - if first_subidentifier < 40: arc0 = 0, arc1 = first_subidentifier
# - elif first_subidentifier < 80: arc0 = 1, arc1 = first_subidentifier - 40
# - else: arc0 = 2, arc1 = first_subidentifier - 80
#
# This problem is not uncommon, see for example https://github.com/randombit/botan/issues/4023
def fixed_decode_object_identifier(data, offset, end_offset):
"""Decode ASN.1 OBJECT IDENTIFIER from bytes to dotted string, fixing large second arc handling."""
def read_subidentifier(data, offset):
value = 0
while True:
b = data[offset]
value = (value << 7) | (b & 0x7F)
offset += 1
if not (b & 0x80):
break
return value, offset
subid, offset = read_subidentifier(data, offset)
if subid < 40:
first = 0
second = subid
elif subid < 80:
first = 1
second = subid - 40
else:
first = 2
second = subid - 80
arcs = [first, second]
while offset < end_offset:
subid, offset = read_subidentifier(data, offset)
arcs.append(subid)
return '.'.join(str(x) for x in arcs)
asn1tools.codecs.ber.decode_object_identifier = fixed_decode_object_identifier
asn1tools.codecs.der.decode_object_identifier = fixed_decode_object_identifier
# test our patch
asn1 = asn1tools.compile_string(fix_asn1_schema, codec='der')
decoded = asn1.decode('TestOid', fix_asn1_encoded)['oid']
assert fix_asn1_oid_string == str(decoded)
fix_asn1_oid_decoding()
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature # noqa: E402
from cryptography import x509 # noqa: E402
from cryptography.exceptions import InvalidSignature # noqa: E402
from cryptography.hazmat.primitives import hashes # noqa: E402
from cryptography.hazmat.primitives.asymmetric import ec, dh # noqa: E402
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption, ParameterFormat # noqa: E402
from pathlib import Path # noqa: E402
import json # noqa: E402
import sys # noqa: E402
import argparse # noqa: E402
import uuid # noqa: E402
import os # noqa: E402
import functools # noqa: E402
from typing import Optional, Dict, List # noqa: E402
from pprint import pprint as pp # noqa: E402
import base64 # noqa: E402
from base64 import b64decode # noqa: E402
from klein import Klein # noqa: E402
from twisted.web.iweb import IRequest # noqa: E402
from osmocom.utils import h2b, b2h, swap_nibbles # noqa: E402
import pySim.esim.rsp as rsp # noqa: E402
from pySim.esim import saip, PMO # noqa: E402
from pySim.esim.es8p import ProfileMetadata,UnprotectedProfilePackage,ProtectedProfilePackage,BoundProfilePackage,BspInstance # noqa: E402
from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id # noqa: E402
from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError # noqa: E402
import logging # noqa: E402
logger = logging.getLogger(__name__)
import pySim.esim.rsp as rsp
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'
@@ -150,173 +53,6 @@ def set_headers(request: IRequest):
request.setHeader('Content-Type', 'application/json;charset=UTF-8')
request.setHeader('X-Admin-Protocol', 'gsma/rsp/v2.1.0')
def validate_request_headers(request: IRequest):
"""Validate mandatory HTTP headers according to SGP.22."""
content_type = request.getHeader('Content-Type')
if not content_type or not content_type.startswith('application/json'):
raise ApiError('1.2.1', '2.1', 'Invalid Content-Type header')
admin_protocol = request.getHeader('X-Admin-Protocol')
if admin_protocol and not admin_protocol.startswith('gsma/rsp/v'):
raise ApiError('1.2.2', '2.1', 'Unsupported X-Admin-Protocol version')
def get_eum_certificate_variant(eum_cert) -> str:
"""Determine EUM certificate variant by checking Certificate Policies extension.
Returns 'O' for old variant, or 'NEW' for Ov3/A/B/C variants."""
try:
cert_policies_ext = eum_cert.extensions.get_extension_for_oid(
x509.oid.ExtensionOID.CERTIFICATE_POLICIES
)
for policy in cert_policies_ext.value:
policy_oid = policy.policy_identifier.dotted_string
logger.debug(f"Found certificate policy: {policy_oid}")
if policy_oid == '2.23.146.1.2.1.2':
logger.debug("Detected EUM certificate variant: O (old)")
return 'O'
elif policy_oid == '2.23.146.1.2.1.0.0.0':
logger.debug("Detected EUM certificate variant: Ov3/A/B/C (new)")
return 'NEW'
except x509.ExtensionNotFound:
logger.debug("No Certificate Policies extension found")
except Exception as e:
logger.debug(f"Error checking certificate policies: {e}")
def parse_permitted_eins_from_cert(eum_cert) -> List[str]:
"""Extract permitted IINs from EUM certificate using the appropriate method
based on certificate variant (O vs Ov3/A/B/C).
Returns list of permitted IINs (basically prefixes that valid EIDs must start with)."""
# Determine certificate variant first
cert_variant = get_eum_certificate_variant(eum_cert)
permitted_iins = []
if cert_variant == 'O':
# Old variant - use nameConstraints extension
permitted_iins.extend(_parse_name_constraints_eins(eum_cert))
else:
# New variants (Ov3, A, B, C) - use GSMA permittedEins extension
permitted_iins.extend(_parse_gsma_permitted_eins(eum_cert))
unique_iins = list(set(permitted_iins))
logger.debug(f"Total unique permitted IINs found: {len(unique_iins)}")
return unique_iins
def _parse_gsma_permitted_eins(eum_cert) -> List[str]:
"""Parse the GSMA permittedEins extension using correct ASN.1 structure.
PermittedEins ::= SEQUENCE OF PrintableString
Each string contains an IIN (Issuer Identification Number) - a prefix of valid EIDs."""
permitted_iins = []
try:
permitted_eins_oid = x509.ObjectIdentifier('2.23.146.1.2.2.0') # sgp26: 2.23.146.1.2.2.0 = ASN1:SEQUENCE:permittedEins
for ext in eum_cert.extensions:
if ext.oid == permitted_eins_oid:
logger.debug(f"Found GSMA permittedEins extension: {ext.oid}")
# Get the DER-encoded extension value
ext_der = ext.value.value if hasattr(ext.value, 'value') else ext.value
if isinstance(ext_der, bytes):
try:
permitted_eins_schema = """
PermittedEins DEFINITIONS ::= BEGIN
PermittedEins ::= SEQUENCE OF PrintableString
END
"""
decoder = asn1tools.compile_string(permitted_eins_schema)
decoded_strings = decoder.decode('PermittedEins', ext_der)
for iin_string in decoded_strings:
# Each string contains an IIN -> prefix of euicc EID
iin_clean = iin_string.strip().upper()
# IINs is 8 chars per sgp22, var len according to sgp29, fortunately we don't care
if (len(iin_clean) == 8 and
all(c in '0123456789ABCDEF' for c in iin_clean) and
len(iin_clean) % 2 == 0):
permitted_iins.append(iin_clean)
logger.debug(f"Found permitted IIN (GSMA): {iin_clean}")
else:
logger.debug(f"Invalid IIN format: {iin_string} (cleaned: {iin_clean})")
except Exception as e:
logger.debug(f"Error parsing GSMA permittedEins extension: {e}")
except Exception as e:
logger.debug(f"Error accessing GSMA certificate extensions: {e}")
return permitted_iins
def _parse_name_constraints_eins(eum_cert) -> List[str]:
"""Parse permitted IINs from nameConstraints extension (variant O)."""
permitted_iins = []
try:
# Look for nameConstraints extension
name_constraints_ext = eum_cert.extensions.get_extension_for_oid(
x509.oid.ExtensionOID.NAME_CONSTRAINTS
)
name_constraints = name_constraints_ext.value
# Check permittedSubtrees for IIN constraints
if name_constraints.permitted_subtrees:
for subtree in name_constraints.permitted_subtrees:
if isinstance(subtree, x509.DirectoryName):
for attribute in subtree.value:
# IINs for O in serialNumber
if attribute.oid == x509.oid.NameOID.SERIAL_NUMBER:
serial_value = attribute.value.upper()
# sgp22 8, sgp29 var len, fortunately we don't care
if (len(serial_value) == 8 and
all(c in '0123456789ABCDEF' for c in serial_value) and
len(serial_value) % 2 == 0):
permitted_iins.append(serial_value)
logger.debug(f"Found permitted IIN (nameConstraints/DN): {serial_value}")
except x509.ExtensionNotFound:
logger.debug("No nameConstraints extension found")
except Exception as e:
logger.debug(f"Error parsing nameConstraints: {e}")
return permitted_iins
def validate_eid_range(eid: str, eum_cert) -> bool:
"""Validate that EID is within the permitted EINs of the EUM certificate."""
if not eid or len(eid) != 32:
logger.debug(f"Invalid EID format: {eid}")
return False
try:
permitted_eins = parse_permitted_eins_from_cert(eum_cert)
if not permitted_eins:
logger.debug("Warning: No permitted EINs found in EUM certificate")
return False
eid_normalized = eid.upper()
logger.debug(f"Validating EID {eid_normalized} against {len(permitted_eins)} permitted EINs")
for permitted_ein in permitted_eins:
if eid_normalized.startswith(permitted_ein):
logger.debug(f"EID {eid_normalized} matches permitted EIN {permitted_ein}")
return True
logger.debug(f"EID {eid_normalized} is not in any permitted EIN list")
return False
except Exception as e:
logger.debug(f"Error validating EID: {e}")
return False
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:
@@ -335,6 +71,12 @@ def build_resp_header(js: dict, status: str = 'Executed-Success', status_code_da
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."""
@@ -382,36 +124,22 @@ class SmDppHttpServer:
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:
logger.debug("cert: %s" % cert)
print("cert: %s" % cert)
subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), cert.extensions))
logger.debug(subject_exts)
print(subject_exts)
subject_pkid = subject_exts[0].value
logger.debug(subject_pkid)
print(subject_pkid)
if subject_pkid and subject_pkid.key_identifier == ci_pkid:
return cert
return None
def validate_certificate_chain_for_verification(self, euicc_ci_pkid_list: List[bytes]) -> bool:
"""Validate that SM-DP+ has valid certificate chains for the given CI PKIDs."""
for ci_pkid in euicc_ci_pkid_list:
ci_cert = self.ci_get_cert_for_pkid(ci_pkid)
if ci_cert:
# Check if our DPauth certificate chains to this CI
try:
cs = CertificateSet(ci_cert)
cs.verify_cert_chain(self.dp_auth.cert)
return True
except VerifyError:
continue
return False
def __init__(self, server_hostname: str, ci_certs_path: str, common_cert_path: str, use_brainpool: bool = False, in_memory: bool = False):
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 = common_cert_path
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'))
@@ -426,15 +154,7 @@ class SmDppHttpServer:
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'))
if in_memory:
self.rss = rsp.RspSessionStore(in_memory=True)
logger.info("Using in-memory session storage")
else:
# Use different session database files for BRP and NIST to avoid file locking during concurrent runs
session_db_suffix = "BRP" if use_brainpool else "NIST"
db_path = os.path.join(DATA_DIR, f"sm-dp-sessions-{session_db_suffix}")
self.rss = rsp.RspSessionStore(filename=db_path, in_memory=False)
logger.info(f"Using file-based session storage: {db_path}")
self.rss = rsp.RspSessionStore(os.path.join(DATA_DIR, "sm-dp-sessions"))
@app.handle_errors(ApiError)
def handle_apierror(self, request: IRequest, failure):
@@ -458,18 +178,17 @@ class SmDppHttpServer:
functionality, such as JSON decoding/encoding and debug-printing."""
@functools.wraps(func)
def _api_wrapper(self, request: IRequest):
validate_request_headers(request)
# TODO: evaluate User-Agent + X-Admin-Protocol header
# TODO: reject any non-JSON Content-type
content = json.loads(request.content.read())
logger.debug("Rx JSON: %s" % json.dumps(content))
print("Rx JSON: %s" % json.dumps(content))
set_headers(request)
output = func(self, request, content)
if output == None:
return ''
output = func(self, request, content) or {}
build_resp_header(output)
logger.debug("Tx JSON: %s" % json.dumps(output))
print("Tx JSON: %s" % json.dumps(output))
return json.dumps(output)
return _api_wrapper
@@ -488,7 +207,7 @@ class SmDppHttpServer:
euiccInfo1_bin = b64decode(content['euiccInfo1'])
euiccInfo1 = rsp.asn1.decode('EUICCInfo1', euiccInfo1_bin)
logger.debug("Rx euiccInfo1: %s" % euiccInfo1)
print("Rx euiccInfo1: %s" % euiccInfo1)
#euiccInfo1['svn']
# TODO: If euiccCiPKIdListForSigningV3 is present ...
@@ -496,12 +215,6 @@ class SmDppHttpServer:
pkid_list = euiccInfo1['euiccCiPKIdListForSigning']
if 'euiccCiPKIdListForSigningV3' in euiccInfo1:
pkid_list = pkid_list + euiccInfo1['euiccCiPKIdListForSigningV3']
# Validate that SM-DP+ supports certificate chains for verification
verification_pkid_list = euiccInfo1.get('euiccCiPKIdListForVerification', [])
if verification_pkid_list and not self.validate_certificate_chain_for_verification(verification_pkid_list):
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')
# verify it supports one of the keys indicated by euiccCiPKIdListForSigning
ci_cert = None
for x in pkid_list:
@@ -516,9 +229,16 @@ class SmDppHttpServer:
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()
transactionId = uuid.uuid4().hex
assert not transactionId in self.rss
# Generate a serverChallenge for eUICC authentication attached to the ongoing RSP session.
@@ -531,9 +251,9 @@ class SmDppHttpServer:
'serverAddress': self.server_hostname,
'serverChallenge': serverChallenge,
}
logger.debug("Tx serverSigned1: %s" % serverSigned1)
print("Tx serverSigned1: %s" % serverSigned1)
serverSigned1_bin = rsp.asn1.encode('ServerSigned1', serverSigned1)
logger.debug("Tx serverSigned1: %s" % rsp.asn1.decode('ServerSigned1', serverSigned1_bin))
print("Tx serverSigned1: %s" % rsp.asn1.decode('ServerSigned1', serverSigned1_bin))
output = {}
output['serverSigned1'] = b64encode2str(serverSigned1_bin)
@@ -562,7 +282,7 @@ class SmDppHttpServer:
authenticateServerResp_bin = b64decode(content['authenticateServerResponse'])
authenticateServerResp = rsp.asn1.decode('AuthenticateServerResponse', authenticateServerResp_bin)
logger.debug("Rx %s: %s" % authenticateServerResp)
print("Rx %s: %s" % authenticateServerResp)
if authenticateServerResp[0] == 'authenticateResponseError':
r_err = authenticateServerResp[1]
#r_err['transactionId']
@@ -571,7 +291,8 @@ class SmDppHttpServer:
r_ok = authenticateServerResp[1]
euiccSigned1 = r_ok['euiccSigned1']
euiccSigned1_bin = rsp.extract_euiccSigned1(authenticateServerResp_bin)
# TODO: use original data, don't re-encode?
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
euiccSignature1_bin = r_ok['euiccSignature1']
euiccCertificate_dec = r_ok['euiccCertificate']
# TODO: use original data, don't re-encode?
@@ -604,32 +325,29 @@ class SmDppHttpServer:
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.1', 'Verification failed')
# 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)')
raise ApiError('8.1', '6.1', 'Verification failed')
# 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
logger.debug("EID (from eUICC cert): %s" % ss.eid)
# Verify EID is within permitted range of EUM certificate
if not validate_eid_range(ss.eid, eum_cert):
raise ApiError('8.1.4', '6.1', 'EID is not within the permitted range of the EUM certificate')
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)')
raise ApiError('8.1', '6.1', 'Verification failed')
# 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.
iccid_str = None
if euiccSigned1['ctxParams1'][0] == 'ctxParamsForCommonAuthentication':
cpca = euiccSigned1['ctxParams1'][1]
matchingId = cpca.get('matchingId', None)
@@ -645,20 +363,11 @@ class SmDppHttpServer:
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 metadata from the profile
# 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)
ss.profileMetadata = ProfileMetadata(iccid_bin= h2b(swap_nibbles('89000123456789012358')), spn="OsmocomSPN", profile_name="OsmocomProfile")
profileMetadata_bin = ss.profileMetadata.gen_store_metadata_request()
# Put together smdpSigned2 + _bin
@@ -694,7 +403,7 @@ class SmDppHttpServer:
prepDownloadResp_bin = b64decode(content['prepareDownloadResponse'])
prepDownloadResp = rsp.asn1.decode('PrepareDownloadResponse', prepDownloadResp_bin)
logger.debug("Rx %s: %s" % prepDownloadResp)
print("Rx %s: %s" % prepDownloadResp)
if prepDownloadResp[0] == 'downloadResponseError':
r_err = prepDownloadResp[1]
@@ -706,7 +415,8 @@ class SmDppHttpServer:
# 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)
# TODO: use original data, don't re-encode?
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
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')
@@ -716,37 +426,37 @@ class SmDppHttpServer:
# store otPK.EUICC.ECKA in session state
ss.euicc_otpk = euiccSigned2['euiccOtpk']
logger.debug("euiccOtpk: %s" % (b2h(ss.euicc_otpk)))
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
logger.debug("curve = %s" % self.dp_pb.get_curve())
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)
logger.debug("smdpOtpk: %s" % b2h(ss.smdp_otpk))
logger.debug("smdpOtsk: %s" % b2h(ss.smdp_ot.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())))
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
# Generate Session Keys using the CRT, opPK.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)
logger.debug("shared_secret: %s" % b2h(ss.shared_secret))
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 debugging the configureISDP step, and we want to avoid
# 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 session keys
# Use sesssion keys
ppp = ProtectedProfilePackage.from_upp(upp, BspInstance(b'\x00'*16, b'\x11'*16, b'\x22'*16))
bpp = BoundProfilePackage.from_ppp(ppp)
@@ -761,52 +471,29 @@ class SmDppHttpServer:
@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)
logger.debug("Rx %s: %s" % pendingNotification)
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:
logger.warning(f"Unable to find session for transactionId: {transactionId}")
return None # Will return HTTP 204 with empty body
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')
logger.debug("Profile Installation Final Result: %s", pird['finalResult'])
print("Unable to verify eUICC signature")
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))
logger.debug("handleNotification: EID %s: %s of %s" % (eid, pmo, iccid))
# TODO
pass
else:
raise ValueError(pendingNotification)
@@ -819,7 +506,7 @@ class SmDppHttpServer:
@rsp_api_wrapper
def cancelSession(self, request: IRequest, content: dict) -> dict:
"""See ES9+ CancelSession in SGP.22 Section 5.6.5"""
logger.debug("Rx JSON: %s" % content)
print("Rx JSON: %s" % content)
transactionId = content['transactionId']
# Verify that the received transactionId is known and relates to an ongoing RSP session
@@ -829,7 +516,7 @@ class SmDppHttpServer:
cancelSessionResponse_bin = b64decode(content['cancelSessionResponse'])
cancelSessionResponse = rsp.asn1.decode('CancelSessionResponse', cancelSessionResponse_bin)
logger.debug("Rx %s: %s" % cancelSessionResponse)
print("Rx %s: %s" % cancelSessionResponse)
if cancelSessionResponse[0] == 'cancelSessionResponseError':
# FIXME: print some error
@@ -861,53 +548,15 @@ class SmDppHttpServer:
def main(argv):
parser = argparse.ArgumentParser()
parser.add_argument("-H", "--host", help="Host/IP to bind HTTP(S) to", default="localhost")
parser.add_argument("-p", "--port", help="TCP port to bind HTTP(S) to", default=443)
parser.add_argument("-c", "--certdir", help=f"cert subdir relative to {DATA_DIR}", default="certs")
parser.add_argument("-s", "--nossl", help="disable built in SSL/TLS support", action='store_true', default=False)
parser.add_argument("-v", "--verbose", help="dump more raw info", action='store_true', default=False)
parser.add_argument("-b", "--brainpool", help="Use Brainpool curves instead of NIST",
action='store_true', default=False)
parser.add_argument("-m", "--in-memory", help="Use ephermal in-memory session storage (for concurrent runs)",
action='store_true', default=False)
#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()
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING)
common_cert_path = os.path.join(DATA_DIR, args.certdir)
hs = SmDppHttpServer(server_hostname=HOSTNAME, ci_certs_path=os.path.join(common_cert_path, 'CertificateIssuer'), common_cert_path=common_cert_path, use_brainpool=args.brainpool)
if(args.nossl):
hs.app.run(args.host, args.port)
else:
curve_type = 'BRP' if args.brainpool else 'NIST'
cert_derpath = Path(common_cert_path) / 'DPtls' / f'CERT_S_SM_DP_TLS_{curve_type}.der'
cert_pempath = Path(common_cert_path) / 'DPtls' / f'CERT_S_SM_DP_TLS_{curve_type}.pem'
cert_skpath = Path(common_cert_path) / 'DPtls' / f'SK_S_SM_DP_TLS_{curve_type}.pem'
dhparam_path = Path(common_cert_path) / "dhparam2048.pem"
if not dhparam_path.exists():
print("Generating dh params, this takes a few seconds..")
# Generate DH parameters with 2048-bit key size and generator 2
parameters = dh.generate_parameters(generator=2, key_size=2048)
pem_data = parameters.parameter_bytes(encoding=Encoding.PEM,format=ParameterFormat.PKCS3)
with open(dhparam_path, 'wb') as file:
file.write(pem_data)
print("DH params created successfully")
if not cert_pempath.exists():
print("Translating tls server cert from DER to PEM..")
with open(cert_derpath, 'rb') as der_file:
der_cert_data = der_file.read()
cert = x509.load_der_x509_certificate(der_cert_data)
pem_cert = cert.public_bytes(Encoding.PEM) #.decode('utf-8')
with open(cert_pempath, 'wb') as pem_file:
pem_file.write(pem_cert)
SERVER_STRING = f'ssl:{args.port}:privateKey={cert_skpath}:certKey={cert_pempath}:dhParameters={dhparam_path}'
print(SERVER_STRING)
hs.app.run(host=HOSTNAME, port=args.port, endpoint_description=SERVER_STRING)
hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=True)
#hs.app.run(endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem")
hs.app.run("localhost", 8000)
if __name__ == "__main__":
main(sys.argv)

View File

@@ -25,7 +25,7 @@
#
import hashlib
import argparse
from optparse import OptionParser
import os
import random
import re
@@ -33,12 +33,11 @@ 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.transport import init_reader
from pySim.legacy.cards import _cards_classes, card_detect
from pySim.utils import derive_milenage_opc, calculate_luhn, dec_iccid
from pySim.utils import h2b, swap_nibbles, rpad, derive_milenage_opc, calculate_luhn, dec_iccid
from pySim.ts_51_011 import EF_AD
from pySim.legacy.ts_51_011 import EF
from pySim.card_handler import *
@@ -47,146 +46,169 @@ 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]",
parser.add_option("--mnclen", dest="mnclen", type="choice",
help="Length of Mobile Network Code [default: %default]",
default="auto",
choices=["2", "3", "auto"],
)
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",
parser.add_option("-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 +219,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 +242,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
@@ -586,7 +613,7 @@ def read_params_csv(opts, imsi=None, iccid=None):
else:
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
# NOTE: We might consider to specify a new CSV field "mnclen" in our
# 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.
@@ -622,9 +649,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)
@@ -725,18 +749,16 @@ def process_card(scc, opts, first, ch):
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:
(res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
iccid = dec_iccid(res)
else:
iccid = opts.iccid
if opts.read_imsi:
elif opts.read_imsi:
(res, _) = scc.read_binary(EF['IMSI'])
imsi = swap_nibbles(res)[3:]
else:

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
#
# Utility to display some information about a SIM card
# Utility to display some informations about a SIM card
#
#
# Copyright (C) 2009 Sylvain Munaut <tnt@246tNt.com>
@@ -29,8 +29,6 @@ import random
import re
import sys
from osmocom.utils import h2b, h2s, swap_nibbles, rpad
from pySim.ts_51_011 import EF_SST_map, EF_AD
from pySim.legacy.ts_51_011 import EF, DF
from pySim.ts_31_102 import EF_UST_map
@@ -42,9 +40,8 @@ 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.ts_51_011 import EF_SMSP
from pySim.utils import h2b, h2s, swap_nibbles, rpad, dec_imsi, dec_iccid, dec_msisdn
from pySim.legacy.utils import format_xplmn_w_act, dec_st
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
@@ -89,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"
@@ -142,15 +139,6 @@ if __name__ == '__main__':
(res, sw) = card.read_record('SMSP', 1)
if sw == '9000':
print("SMSP: %s" % (res,))
ef_smsp = EF_SMSP()
smsc_a = ef_smsp.decode_record_bin(h2b(res), 1).get('tp_sc_addr', {})
smsc_n = smsc_a.get('call_number', None)
if smsc_a.get('ton_npi', {}).get('type_of_number', None) == 'international' and smsc_n is not None:
smsc = '+' + smsc_n
else:
smsc = smsc_n
if smsc is not None:
print("SMSC: %s" % (smsc,))
else:
print("SMSP: Can't read, response code = %s" % (sw,))

File diff suppressed because it is too large Load Diff

View File

@@ -1,428 +0,0 @@
#!/usr/bin/env python3
#
# Program to emulate the entire communication path SMSC-MSC-BSC-BTS-ME
# that is usually between an OTA backend and the SIM card. This allows
# to play with SIM OTA technology without using a mobile network or even
# a mobile phone.
#
# An external application must encode (and encrypt/sign) the OTA SMS
# and submit them via SMPP to this program, just like it would submit
# it normally to a SMSC (SMS Service Centre). The program then re-formats
# the SMPP-SUBMIT into a SMS DELIVER TPDU and passes it via an ENVELOPE
# APDU to the SIM card that is locally inserted into a smart card reader.
#
# The path from SIM to external OTA application works the opposite way.
# (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 argparse
import logging
import colorlog
from twisted.protocols import basic
from twisted.internet import defer, endpoints, protocol, reactor, task
from twisted.cred.portal import IRealm
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
from twisted.cred.portal import Portal
from zope.interface import implementer
from smpp.twisted.config import SMPPServerConfig
from smpp.twisted.server import SMPPServerFactory, SMPPBindManager
from smpp.twisted.protocol import SMPPSessionStates, DataHandlerResponse
from smpp.pdu import pdu_types, operations, pdu_encoding
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
from pySim.transport import LinkBase, ProactiveHandler, argparse_add_reader_args, init_reader, ApduTracer
from pySim.commands import SimCardCommands
from pySim.cards import UiccCardBase
from pySim.exceptions import *
from pySim.cat import ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload, BearerDescription
from pySim.cat import DeviceIdentities, Address, OtherAddress, UiccTransportLevel, BufferSize
from pySim.cat import ChannelStatus, ChannelData, ChannelDataLength
from pySim.utils import b2h, h2b
logger = logging.getLogger(__name__)
# MSISDNs to use when generating proactive SMS messages
SIM_MSISDN='23'
ESME_MSISDN='12'
# HACK: we need some kind of mapping table between system_id and card-reader
# or actually route based on MSISDNs
hackish_global_smpp = None
class MyApduTracer(ApduTracer):
def trace_response(self, cmd, sw, resp):
print("-> %s %s" % (cmd[:10], cmd[10:]))
print("<- %s: %s" % (sw, resp))
class TcpProtocol(protocol.Protocol):
def dataReceived(self, data):
pass
def connectionLost(self, reason):
pass
def tcp_connected_callback(p: protocol.Protocol):
"""called by twisted TCP client."""
logger.error("%s: connected!" % p)
class ProactChannel:
"""Representation of a single protective channel."""
def __init__(self, channels: 'ProactChannels', chan_nr: int):
self.channels = channels
self.chan_nr = chan_nr
self.ep = None
def close(self):
"""Close the channel."""
if self.ep:
self.ep.disconnect()
self.channels.channel_delete(self.chan_nr)
class ProactChannels:
"""Wrapper class for maintaining state of proactive channels."""
def __init__(self):
self.channels = {}
def channel_create(self) -> ProactChannel:
"""Create a new proactive channel, allocating its integer number."""
for i in range(1, 9):
if not i in self.channels:
self.channels[i] = ProactChannel(self, i)
return self.channels[i]
raise ValueError('Cannot allocate another channel: All channels active')
def channel_delete(self, chan_nr: int):
del self.channels[chan_nr]
class Proact(ProactiveHandler):
#def __init__(self, smpp_factory):
# self.smpp_factory = smpp_factory
def __init__(self):
self.channels = ProactChannels()
@staticmethod
def _find_first_element_of_type(instlist, cls):
for i in instlist:
if isinstance(i, cls):
return i
return None
"""Call-back which the pySim transport core calls whenever it receives a
proactive command from the SIM."""
def handle_SendShortMessage(self, pcmd: ProactiveCommand):
# {'smspp_download': [{'device_identities': {'source_dev_id': 'network',
# 'dest_dev_id': 'uicc'}},
# {'address': {'ton_npi': {'ext': True,
# 'type_of_number': 'international',
# 'numbering_plan_id': 'isdn_e164'},
# 'call_number': '79'}},
# {'sms_tpdu': {'tpdu': '40048111227ff6407070611535004d02700000481516011212000001fe4c0943aea42e45021c078ae06c66afc09303608874b72f58bacadb0dcf665c29349c799fbb522e61709c9baf1890015e8e8e196e36153106c8b92f95153774'}}
# ]}
"""Card requests sending a SMS. We need to pass it on to the ESME via SMPP."""
logger.info("SendShortMessage")
logger.info(pcmd)
# Relevant parts in pcmd: Address, SMS_TPDU
addr_ie = Proact._find_first_element_of_type(pcmd.children, Address)
sms_tpdu_ie = Proact._find_first_element_of_type(pcmd.children, SMS_TPDU)
raw_tpdu = sms_tpdu_ie.decoded['tpdu']
submit = SMS_SUBMIT.from_bytes(raw_tpdu)
submit.tp_da = AddressField(addr_ie.decoded['call_number'], addr_ie.decoded['ton_npi']['type_of_number'],
addr_ie.decoded['ton_npi']['numbering_plan_id'])
logger.info(submit)
self.send_sms_via_smpp(submit)
def handle_OpenChannel(self, pcmd: ProactiveCommand):
"""Card requests opening a new channel via a UDP/TCP socket."""
# {'open_channel': [{'command_details': {'command_number': 1,
# 'type_of_command': 'open_channel',
# 'command_qualifier': 3}},
# {'device_identities': {'source_dev_id': 'uicc',
# 'dest_dev_id': 'terminal'}},
# {'bearer_description': {'bearer_type': 'default',
# 'bearer_parameters': ''}},
# {'buffer_size': 1024},
# {'uicc_transport_level': {'protocol_type': 'tcp_uicc_client_remote',
# 'port_number': 32768}},
# {'other_address': {'type_of_address': 'ipv4',
# 'address': '01020304'}}
# ]}
logger.info("OpenChannel")
logger.info(pcmd)
transp_lvl_ie = Proact._find_first_element_of_type(pcmd.children, UiccTransportLevel)
other_addr_ie = Proact._find_first_element_of_type(pcmd.children, OtherAddress)
bearer_desc_ie = Proact._find_first_element_of_type(pcmd.children, BearerDescription)
buffer_size_ie = Proact._find_first_element_of_type(pcmd.children, BufferSize)
if transp_lvl_ie.decoded['protocol_type'] != 'tcp_uicc_client_remote':
raise ValueError('Unsupported protocol_type')
if other_addr_ie.decoded.get('type_of_address', None) != 'ipv4':
raise ValueError('Unsupported type_of_address')
ipv4_bytes = h2b(other_addr_ie.decoded['address'])
ipv4_str = '%u.%u.%u.%u' % (ipv4_bytes[0], ipv4_bytes[1], ipv4_bytes[2], ipv4_bytes[3])
port_nr = transp_lvl_ie.decoded['port_number']
print("%s:%u" % (ipv4_str, port_nr))
channel = self.channels.channel_create()
channel.ep = endpoints.TCP4ClientEndpoint(reactor, ipv4_str, port_nr)
channel.prot = TcpProtocol()
d = endpoints.connectProtocol(channel.ep, channel.prot)
# FIXME: why is this never called despite the client showing the inbound connection?
d.addCallback(tcp_connected_callback)
# Terminal Response example: [
# {'command_details': {'command_number': 1,
# 'type_of_command': 'open_channel',
# 'command_qualifier': 3}},
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
# {'channel_status': '8100'},
# {'bearer_description': {'bearer_type': 'default', 'bearer_parameters': ''}},
# {'buffer_size': 1024}
# ]
return self.prepare_response(pcmd) + [ChannelStatus(decoded='8100'), bearer_desc_ie, buffer_size_ie]
def handle_CloseChannel(self, pcmd: ProactiveCommand):
"""Close a channel."""
logger.info("CloseChannel")
logger.info(pcmd)
def handle_ReceiveData(self, pcmd: ProactiveCommand):
"""Receive/read data from the socket."""
# {'receive_data': [{'command_details': {'command_number': 1,
# 'type_of_command': 'receive_data',
# 'command_qualifier': 0}},
# {'device_identities': {'source_dev_id': 'uicc',
# 'dest_dev_id': 'channel_1'}},
# {'channel_data_length': 9}
# ]}
logger.info("ReceiveData")
logger.info(pcmd)
# Terminal Response example: [
# {'command_details': {'command_number': 1,
# 'type_of_command': 'receive_data',
# 'command_qualifier': 0}},
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
# {'channel_data': '16030100040e000000'},
# {'channel_data_length': 0}
# ]
return self.prepare_response(pcmd) + []
def handle_SendData(self, pcmd: ProactiveCommand):
"""Send/write data received from the SIM to the socket."""
# {'send_data': [{'command_details': {'command_number': 1,
# 'type_of_command': 'send_data',
# 'command_qualifier': 1}},
# {'device_identities': {'source_dev_id': 'uicc',
# 'dest_dev_id': 'channel_1'}},
# {'channel_data': '160301003c010000380303d0f45e12b52ce5bb522750dd037738195334c87a46a847fe2b6886cada9ea6bf00000a00ae008c008b00b0002c010000050001000101'}
# ]}
logger.info("SendData")
logger.info(pcmd)
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
chan_data_ie = Proact._find_first_element_of_type(pcmd.children, ChannelData)
chan_str = dev_id_ie.decoded['dest_dev_id']
chan_nr = 1 # FIXME
chan = self.channels.channels.get(chan_nr, None)
# FIXME chan.prot.transport.write(h2b(chan_data_ie.decoded))
# Terminal Response example: [
# {'command_details': {'command_number': 1,
# 'type_of_command': 'send_data',
# 'command_qualifier': 1}},
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
# {'channel_data_length': 255}
# ]
return self.prepare_response(pcmd) + [ChannelDataLength(decoded=255)]
def handle_SetUpEventList(self, pcmd: ProactiveCommand):
# {'set_up_event_list': [{'command_details': {'command_number': 1,
# 'type_of_command': 'set_up_event_list',
# 'command_qualifier': 0}},
# {'device_identities': {'source_dev_id': 'uicc',
# 'dest_dev_id': 'terminal'}},
# {'event_list': ['data_available', 'channel_status']}
# ]}
logger.info("SetUpEventList")
logger.info(pcmd)
# Terminal Response example: [
# {'command_details': {'command_number': 1,
# 'type_of_command': 'set_up_event_list',
# 'command_qualifier': 0}},
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}}
# ]
return self.prepare_response(pcmd)
def getChannelStatus(self, pcmd: ProactiveCommand):
logger.info("GetChannelStatus")
logger.info(pcmd)
return self.prepare_response(pcmd) + []
def send_sms_via_smpp(self, submit: SMS_SUBMIT):
# while in a normal network the phone/ME would *submit* a message to the SMSC,
# we are actually emulating the SMSC itself, so we must *deliver* the message
# to the ESME
deliver = SMS_DELIVER.from_submit(submit)
deliver_smpp = deliver.to_smpp()
hackish_global_smpp.sendDataRequest(deliver_smpp)
# # obtain the connection/binding of system_id to be used for delivering MO-SMS to the ESME
# connection = smpp_server.getBoundConnections[system_id].getNextBindingForDelivery()
# connection.sendDataRequest(deliver_smpp)
def dcs_is_8bit(dcs):
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-smpp2sim.py:150: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
class MyServer:
@implementer(IRealm)
class SmppRealm:
def requestAvatar(self, avatarId, mind, *interfaces):
return ('SMPP', avatarId, lambda: None)
def __init__(self, tcp_port:int = 2775, bind_ip = '::', system_id:str = 'test', password:str = 'test'):
smpp_config = SMPPServerConfig(msgHandler=self._msgHandler,
systems={system_id: {'max_bindings': 2}})
portal = Portal(self.SmppRealm())
credential_checker = InMemoryUsernamePasswordDatabaseDontUse()
credential_checker.addUser(system_id, password)
portal.registerChecker(credential_checker)
self.factory = SMPPServerFactory(smpp_config, auth_portal=portal)
logger.info('Binding Virtual SMSC to TCP Port %u at %s' % (tcp_port, bind_ip))
smppEndpoint = endpoints.TCP6ServerEndpoint(reactor, tcp_port, interface=bind_ip)
smppEndpoint.listen(self.factory)
self.tp = self.scc = self.card = None
def connect_to_card(self, tp: LinkBase):
self.tp = tp
self.scc = SimCardCommands(self.tp)
self.card = UiccCardBase(self.scc)
# this should be part of UiccCardBase, but FairewavesSIM breaks with that :/
self.scc.cla_byte = "00"
self.scc.sel_ctrl = "0004"
self.card.read_aids()
self.card.select_adf_by_aid(adf='usim')
# FIXME: create a more realistic profile than ffffff
self.scc.terminal_profile('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
def _msgHandler(self, system_id, smpp, pdu):
"""Handler for incoming messages received via SMPP from ESME."""
# HACK: we need some kind of mapping table between system_id and card-reader
# or actually route based on MSISDNs
global hackish_global_smpp
hackish_global_smpp = smpp
if pdu.id == pdu_types.CommandId.submit_sm:
return self.handle_submit_sm(system_id, smpp, pdu)
else:
logger.warning('Rejecting non-SUBMIT commandID')
return pdu_types.CommandStatus.ESME_RINVCMDID
def handle_submit_sm(self, system_id, smpp, pdu):
"""SUBMIT-SM was received via SMPP from ESME. We need to deliver it to the SIM."""
# check for valid data coding scheme + PID
if not dcs_is_8bit(pdu.params['data_coding']):
logger.warning('Rejecting non-8bit DCS')
return pdu_types.CommandStatus.ESME_RINVDCS
if pdu.params['protocol_id'] != 0x7f:
logger.warning('Rejecting non-SIM PID')
return pdu_types.CommandStatus.ESME_RINVDCS
# 1) build a SMS-DELIVER (!) from the SMPP-SUBMIT
tpdu = SMS_DELIVER.from_smpp_submit(pdu)
logger.info(tpdu)
# 2) wrap into the CAT ENVELOPE for SMS-PP-Download
tpdu_ie = SMS_TPDU(decoded={'tpdu': b2h(tpdu.to_bytes())})
addr_ie = Address(decoded={'ton_npi': {'ext':False, 'type_of_number':'unknown', 'numbering_plan_id':'unknown'}, 'call_number': '0123456'})
dev_ids = DeviceIdentities(decoded={'source_dev_id': 'network', 'dest_dev_id': 'uicc'})
sms_dl = SMSPPDownload(children=[dev_ids, addr_ie, tpdu_ie])
# 3) send to the card
envelope_hex = b2h(sms_dl.to_tlv())
logger.info("ENVELOPE: %s" % envelope_hex)
(data, sw) = self.scc.envelope(envelope_hex)
logger.info("SW %s: %s" % (sw, data))
if sw in ['9200', '9300']:
# TODO send back RP-ERROR message with TP-FCS == 'SIM Application Toolkit Busy'
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
elif sw == '9000' or sw[0:2] in ['6f', '62', '63'] and len(data):
# data something like 027100000e0ab000110000000000000001612f or
# 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
# which is the user-data portion of the SMS starting with the UDH (027100)
# TODO: return the response back to the sender in an RP-ACK; PID/DCS like in CMD
deliver = operations.DeliverSM(service_type=pdu.params['service_type'],
source_addr_ton=pdu.params['dest_addr_ton'],
source_addr_npi=pdu.params['dest_addr_npi'],
source_addr=pdu.params['destination_addr'],
dest_addr_ton=pdu.params['source_addr_ton'],
dest_addr_npi=pdu.params['source_addr_npi'],
destination_addr=pdu.params['source_addr'],
esm_class=pdu.params['esm_class'],
protocol_id=pdu.params['protocol_id'],
priority_flag=pdu.params['priority_flag'],
data_coding=pdu.params['data_coding'],
short_message=h2b(data))
smpp.sendDataRequest(deliver)
return pdu_types.CommandStatus.ESME_ROK
else:
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
option_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
argparse_add_reader_args(option_parser)
smpp_group = option_parser.add_argument_group('SMPP Options')
smpp_group.add_argument('--smpp-bind-port', type=int, default=2775,
help='TCP Port to bind the SMPP socket to')
smpp_group.add_argument('--smpp-bind-ip', default='::',
help='IPv4/IPv6 address to bind the SMPP socket to')
smpp_group.add_argument('--smpp-system-id', default='test',
help='SMPP System-ID used by ESME to bind')
smpp_group.add_argument('--smpp-password', default='test',
help='SMPP Password used by ESME to bind')
if __name__ == '__main__':
log_format='%(log_color)s%(levelname)-8s%(reset)s %(name)s: %(message)s'
colorlog.basicConfig(level=logging.INFO, format = log_format)
logger = colorlog.getLogger()
opts = option_parser.parse_args()
tp = init_reader(opts, proactive_handler = Proact())
if tp is None:
exit(1)
tp.connect()
global g_ms
g_ms = MyServer(opts.smpp_bind_port, opts.smpp_bind_ip, opts.smpp_system_id, opts.smpp_password)
g_ms.connect_to_card(tp)
reactor.run()

View File

@@ -8,22 +8,17 @@ from pprint import pprint as pp
from pySim.apdu import *
from pySim.runtime import RuntimeState
from osmocom.utils import JsonEncoder
from pySim.cards import UiccCardBase
from pySim.commands import SimCardCommands
from pySim.profile import CardProfile
from pySim.ts_102_221 import CardProfileUICC
from pySim.ts_31_102 import CardApplicationUSIM
from pySim.ts_31_103 import CardApplicationISIM
from pySim.euicc import CardApplicationISDR, CardApplicationECASD
from pySim.transport import LinkBase
from pySim.apdu_source.gsmtap import GsmtapApduSource
from pySim.apdu_source.pyshark_rspro import PysharkRsproPcap, PysharkRsproLive
from pySim.apdu_source.pyshark_gsmtap import PysharkGsmtapPcap
from pySim.apdu_source.tca_loader_log import TcaLoaderLogApduSource
from pySim.apdu_source.stdin_hex import StdinHexApduSource
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
@@ -56,7 +51,7 @@ class DummySimLink(LinkBase):
def __str__(self):
return "dummy"
def _send_apdu(self, pdu):
def _send_apdu_raw(self, pdu):
#print("DummySimLink-apdu: %s" % pdu)
return [], '9000'
@@ -66,7 +61,7 @@ class DummySimLink(LinkBase):
def disconnect(self):
pass
def _reset_card(self):
def reset_card(self):
return 1
def get_atr(self):
@@ -83,8 +78,6 @@ class Tracer:
profile = CardProfileUICC()
profile.add_application(CardApplicationUSIM())
profile.add_application(CardApplicationISIM())
profile.add_application(CardApplicationISDR())
profile.add_application(CardApplicationECASD())
scc = SimCardCommands(transport=DummySimLink())
card = UiccCardBase(scc)
self.rs = RuntimeState(card, profile)
@@ -100,8 +93,7 @@ class Tracer:
"""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("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id, inst.col_sw, inst.processed))
print("===============================")
def format_reset(self, apdu: CardReset):
@@ -152,7 +144,7 @@ global_group.add_argument('--no-suppress-select', action='store_false', dest='su
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 response to the most recent SEELCT.""")
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.""")
@@ -186,15 +178,6 @@ parser_rspro_pyshark_live = subparsers.add_parser('rspro-pyshark-live', help="""
parser_rspro_pyshark_live.add_argument('-i', '--interface', required=True,
help='Name of the network interface to capture on')
parser_tcaloader_log = subparsers.add_parser('tca-loader-log', help="""
Read APDUs from a TCA Loader log file.""")
parser_tcaloader_log.add_argument('-f', '--log-file', required=True,
help='Name of the log file to be read')
parser_stdin_hex = subparsers.add_parser('stdin-hex', help="""
Read APDUs as hex-string from stdin.""")
if __name__ == '__main__':
opts = option_parser.parse_args()
@@ -208,12 +191,6 @@ if __name__ == '__main__':
s = PysharkRsproLive(opts.interface)
elif opts.source == 'gsmtap-pyshark-pcap':
s = PysharkGsmtapPcap(opts.pcap_file)
elif opts.source == 'tca-loader-log':
s = TcaLoaderLogApduSource(opts.log_file)
elif opts.source == 'stdin-hex':
s = StdinHexApduSource()
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)

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,14 +27,14 @@ 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
from construct import Optional as COptional
from osmocom.construct import *
from osmocom.utils import *
from construct import *
from construct import Optional as COptional
from pySim.construct import *
from pySim.utils import *
from pySim.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
@@ -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))
@@ -149,10 +150,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 +187,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 +236,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 +275,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 +285,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 +295,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 +315,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 +428,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):
@@ -461,6 +452,7 @@ class CardReset:
self.atr = atr
def __str__(self):
if self.atr:
if (self.atr):
return '%s(%s)' % (type(self).__name__, b2h(self.atr))
return '%s' % (type(self).__name__)
else:
return '%s' % (type(self).__name__)

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,12 @@ 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__)
@@ -100,7 +94,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
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:
@@ -109,6 +103,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
#print("\tSELECT AID %s" % adf)
else:
logger.warning('SELECT UNKNOWN AID %s', aid)
pass
else:
raise ValueError('Select Mode %s not implemented' % mode)
# decode the SELECT response
@@ -116,7 +111,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 +124,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 = {}
@@ -295,9 +290,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 +307,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 +319,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 +330,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 +342,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):
@@ -397,12 +395,13 @@ class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X',
manage_channel.add_lchan(created_channel_nr)
self.col_id = '%02u' % created_channel_nr
return {'mode': mode, 'created_channel': created_channel_nr }
if mode == 'close_channel':
elif mode == 'close_channel':
closed_channel_nr = self.cmd_dict['p2']['logical_channel_number']
rs.del_lchan(closed_channel_nr)
self.col_id = '%02u' % closed_channel_nr
return {'mode': mode, 'closed_channel': closed_channel_nr }
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 +419,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 +436,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 +460,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']):

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, 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'))

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']:
elif sub_type in ['pps_req', 'pps_rsp']:
# simply ignore for now
pass
else:

View File

@@ -16,20 +16,21 @@
# 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 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 pySim.utils import h2b, b2h
from pySim.apdu import Tpdu
from pySim.gsmtap import GsmtapMessage
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
logger = logging.getLogger(__name__)
@@ -66,10 +67,10 @@ class _PysharkGsmtap(ApduSource):
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']:
elif sub_type in ['pps_req', 'pps_rsp']:
# simply ignore for now
pass
else:
@@ -84,5 +85,6 @@ class PysharkGsmtapPcap(_PysharkGsmtap):
Args:
pcap_filename: File name of the pcap file to be opened
"""
pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='gsm_sim || iso7816.atr', use_json=True, keep_packets=False)
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

View File

@@ -1,39 +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 StdinHexApduSource(ApduSource):
"""ApduSource for reading apdu hex-strings from stdin."""
def read_packet(self) -> PacketType:
while True:
command = input("C-APDU >")
if len(command) == 0:
continue
response = '9000'
return ApduCommands.parse_cmd_bytes(h2b(command) + h2b(response))

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

@@ -19,13 +19,13 @@ 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.cards import card_detect, SimCardBase, UiccCardBase
from pySim.exceptions import NoCardError
from pySim.runtime import RuntimeState
from pySim.profile import CardProfile
from pySim.cdma_ruim import CardProfileRUIM
from pySim.ts_102_221 import CardProfileUICC
from pySim.utils import all_subclasses
from pySim.exceptions import SwMatchError
# we need to import this module so that the SysmocomSJA2 sub-class of
# CardModel is created, which will add the ATR-based matching and
@@ -42,7 +42,7 @@ import pySim.ara_m
import pySim.global_platform
import pySim.euicc
def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState, SimCardBase]:
def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
"""
Detect card in reader and setup card profile and runtime state. This
function must be called at least once on startup. The card and runtime
@@ -57,12 +57,6 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
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:
@@ -113,16 +107,7 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
# 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,29 +26,28 @@ Support for the Secure Element Access Control, specifically the ARA-M inside an
#
from construct import 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 *
from pySim.tlv import *
import pySim.global_platform
# various BER-TLV encoded Data Objects (DOs)
class AidRefDO(BER_TLV_IE, tag=0x4f):
# GPD_SPE_013 v1.1 Table 6-3
# SEID v1.1 Table 6-3
_construct = HexAdapter(GreedyBytes)
class AidRefEmptyDO(BER_TLV_IE, tag=0xc0):
# GPD_SPE_013 v1.1 Table 6-3
# SEID v1.1 Table 6-3
pass
class DevAppIdRefDO(BER_TLV_IE, tag=0xc1):
# GPD_SPE_013 v1.1 Table 6-4
# SEID v1.1 Table 6-4
_construct = HexAdapter(GreedyBytes)
@@ -58,39 +57,41 @@ class PkgRefDO(BER_TLV_IE, tag=0xca):
class RefDO(BER_TLV_IE, tag=0xe1, nested=[AidRefDO, AidRefEmptyDO, DevAppIdRefDO, PkgRefDO]):
# GPD_SPE_013 v1.1 Table 6-5
# SEID v1.1 Table 6-5
pass
class ApduArDO(BER_TLV_IE, tag=0xd0):
# GPD_SPE_013 v1.1 Table 6-8
# SEID v1.1 Table 6-8
def _from_bytes(self, do: bytes):
if len(do) == 1:
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')
@@ -108,7 +109,7 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
class NfcArDO(BER_TLV_IE, tag=0xd1):
# GPD_SPE_013 v1.1 Table 6-9
# SEID v1.1 Table 6-9
_construct = Struct('nfc_event_access_rule' /
Enum(Int8ub, never=0, always=1))
@@ -120,122 +121,124 @@ class PermArDO(BER_TLV_IE, tag=0xdb):
class ArDO(BER_TLV_IE, tag=0xe3, nested=[ApduArDO, NfcArDO, PermArDO]):
# GPD_SPE_013 v1.1 Table 6-7
# SEID v1.1 Table 6-7
pass
class RefArDO(BER_TLV_IE, tag=0xe2, nested=[RefDO, ArDO]):
# GPD_SPE_013 v1.1 Table 6-6
# SEID v1.1 Table 6-6
pass
class ResponseAllRefArDO(BER_TLV_IE, tag=0xff40, nested=[RefArDO]):
# GPD_SPE_013 v1.1 Table 4-2
# SEID v1.1 Table 4-2
pass
class ResponseArDO(BER_TLV_IE, tag=0xff50, nested=[ArDO]):
# GPD_SPE_013 v1.1 Table 4-3
# SEID v1.1 Table 4-3
pass
class ResponseRefreshTagDO(BER_TLV_IE, tag=0xdf20):
# GPD_SPE_013 v1.1 Table 4-4
# SEID v1.1 Table 4-4
_construct = Struct('refresh_tag'/HexAdapter(Bytes(8)))
class DeviceInterfaceVersionDO(BER_TLV_IE, tag=0xe6):
# GPD_SPE_013 v1.1 Table 6-12
# SEID v1.1 Table 6-12
_construct = Struct('major'/Int8ub, 'minor'/Int8ub, 'patch'/Int8ub)
class DeviceConfigDO(BER_TLV_IE, tag=0xe4, nested=[DeviceInterfaceVersionDO]):
# GPD_SPE_013 v1.1 Table 6-10
# SEID v1.1 Table 6-10
pass
class ResponseDeviceConfigDO(BER_TLV_IE, tag=0xff7f, nested=[DeviceConfigDO]):
# GPD_SPE_013 v1.1 Table 5-14
# SEID v1.1 Table 5-14
pass
class AramConfigDO(BER_TLV_IE, tag=0xe5, nested=[DeviceInterfaceVersionDO]):
# GPD_SPE_013 v1.1 Table 6-11
# SEID v1.1 Table 6-11
pass
class ResponseAramConfigDO(BER_TLV_IE, tag=0xdf21, nested=[AramConfigDO]):
# GPD_SPE_013 v1.1 Table 4-5
# SEID v1.1 Table 4-5
pass
class CommandStoreRefArDO(BER_TLV_IE, tag=0xf0, nested=[RefArDO]):
# GPD_SPE_013 v1.1 Table 5-2
# SEID v1.1 Table 5-2
pass
class CommandDelete(BER_TLV_IE, tag=0xf1, nested=[AidRefDO, AidRefEmptyDO, RefDO, RefArDO]):
# GPD_SPE_013 v1.1 Table 5-4
# SEID v1.1 Table 5-4
pass
class CommandUpdateRefreshTagDO(BER_TLV_IE, tag=0xf2):
# GPD_SPE_013 V1.1 Table 5-6
# SEID V1.1 Table 5-6
pass
class CommandRegisterClientAidsDO(BER_TLV_IE, tag=0xf7, nested=[AidRefDO, AidRefEmptyDO]):
# GPD_SPE_013 v1.1 Table 5-7
# SEID v1.1 Table 5-7
pass
class CommandGet(BER_TLV_IE, tag=0xf3, nested=[AidRefDO, AidRefEmptyDO]):
# GPD_SPE_013 v1.1 Table 5-8
# SEID v1.1 Table 5-8
pass
class CommandGetAll(BER_TLV_IE, tag=0xf4):
# GPD_SPE_013 v1.1 Table 5-9
# SEID v1.1 Table 5-9
pass
class CommandGetClientAidsDO(BER_TLV_IE, tag=0xf6):
# GPD_SPE_013 v1.1 Table 5-10
# SEID v1.1 Table 5-10
pass
class CommandGetNext(BER_TLV_IE, tag=0xf5):
# GPD_SPE_013 v1.1 Table 5-11
# SEID v1.1 Table 5-11
pass
class CommandGetDeviceConfigDO(BER_TLV_IE, tag=0xf8):
# GPD_SPE_013 v1.1 Table 5-12
# SEID v1.1 Table 5-12
pass
class ResponseAracAidDO(BER_TLV_IE, tag=0xff70, nested=[AidRefDO, AidRefEmptyDO]):
# GPD_SPE_013 v1.1 Table 5-13
# SEID v1.1 Table 5-13
pass
class BlockDO(BER_TLV_IE, tag=0xe7):
# GPD_SPE_013 v1.1 Table 6-13
# SEID v1.1 Table 6-13
_construct = Struct('offset'/Int16ub, 'length'/Int8ub)
# GPD_SPE_013 v1.1 Table 4-1
# SEID v1.1 Table 4-1
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
pass
# SEID v1.1 Table 4-2
# GPD_SPE_013 v1.1 Table 4-2
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
ResponseRefreshTagDO, ResponseAramConfigDO]):
pass
# SEID v1.1 Table 5-1
# GPD_SPE_013 v1.1 Table 5-1
class StoreCommandDoCollection(TLV_IE_Collection,
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
@@ -244,7 +247,7 @@ class StoreCommandDoCollection(TLV_IE_Collection,
pass
# GPD_SPE_013 v1.1 Section 5.1.2
# SEID v1.1 Section 5.1.2
class StoreResponseDoCollection(TLV_IE_Collection,
nested=[ResponseAllRefArDO, ResponseAracAidDO, ResponseDeviceConfigDO]):
pass
@@ -262,7 +265,7 @@ class ADF_ARAM(CardADF):
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,55 +277,59 @@ 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([{'device_interface_version_do': {
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
return ADF_ARAM.xceive_apdu_tlv(tp, '80cadf21', cmd_do, ResponseAramConfigDO)
@with_default_category('Application-Specific Commands')
class AddlShellCommands(CommandSet):
def 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.lchan.scc._tp)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
def do_aram_get_config(self, _opts):
def do_aram_get_config(self, opts):
"""Perform GET DATA [Config] on the ARA-M Applet: Tell it our version and retrieve its version."""
res_do = ADF_ARAM.get_config(self._cmd.lchan.scc)
res_do = ADF_ARAM.get_config(self._cmd.lchan.scc._tp)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
store_ref_ar_do_parse = argparse.ArgumentParser()
# REF-DO
store_ref_ar_do_parse.add_argument(
'--device-app-id', required=True, help='Identifies the specific device application that the rule applies to. Hash of Certificate of Application Provider, or UUID. (20/32 hex bytes)')
'--device-app-id', required=True, help='Identifies the specific device application that the rule appplies to. Hash of Certificate of Application Provider, or UUID. (20/32 hex bytes)')
aid_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
aid_grp.add_argument(
'--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 or 0 hex bytes)')
'--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 hex bytes)')
aid_grp.add_argument('--aid-empty', action='store_true',
help='No specific SE application, applies to implicitly selected application (all channels)')
help='No specific SE application, applies to all applications')
store_ref_ar_do_parse.add_argument(
'--pkg-ref', help='Full Android Java package name (up to 127 chars ASCII)')
# AR-DO
@@ -332,7 +339,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,7 +353,7 @@ 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:
if opts.aid != None:
ref_do_content += [{'aid_ref_do': opts.aid}]
elif opts.aid_empty:
ref_do_content += [{'aid_ref_empty_do': None}]
@@ -356,19 +363,12 @@ class ADF_ARAM(CardADF):
# AR
ar_do_content = []
if opts.apdu_never:
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'never'}}]
ar_do_content += [{'apdu_ar_od': {'generic_access_rule': 'never'}}]
elif opts.apdu_always:
ar_do_content += [{'apdu_ar_do': {'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 += [{'apdu_ar_do': {'apdu_filter': [opts.apdu_filter]}}]
if opts.nfc_always:
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
elif opts.nfc_never:
@@ -377,29 +377,24 @@ class ADF_ARAM(CardADF):
ar_do_content += [{'perm_ar_do': {'permissions': opts.android_permissions}}]
d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
csrado = CommandStoreRefArDO()
csrado.from_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.lchan.scc._tp, csrado)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
def do_aram_delete_all(self, _opts):
def do_aram_delete_all(self, opts):
"""Perform STORE DATA [Command-Delete[all]] to delete all access rules."""
deldo = CommandDelete()
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, deldo)
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, deldo)
if res_do:
self._cmd.poutput_json(res_do.to_dict())
def do_aram_lock(self, opts):
"""Lock STORE DATA command to prevent unauthorized changes
(Proprietary feature that is specific to sysmocom's fork of Bertrand Martels ARA-M implementation.)"""
self._cmd.lchan.scc.send_apdu_checksw('80e2900001A1', '9000')
# SEAC v1.1 Section 4.1.2.2 + 5.1.2.2
sw_aram = {
'ARA-M': {
'6381': 'Rule successfully stored but an access rule already exists',
'6382': 'Rule successfully stored but contained at least one unknown (discarded) BER-TLV',
'6382': 'Rule successfully stored bu contained at least one unknown (discarded) BER-TLV',
'6581': 'Memory Problem',
'6700': 'Wrong Length in Lc',
'6981': 'DO is not supported by the ARA-M/ARA-C',
@@ -420,84 +415,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
for d in dictlist:
if key in d:
obj = d.get(key)
if obj is None:
return ""
return obj
return None
@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)
aid_ref_empty_do = CardApplicationARAM.__export_get_from_dictlist('aid_ref_empty_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 is not None and len(aid_ref_do) > 0:
export_str += (" --aid %s" % aid_ref_do)
elif aid_ref_do is not None:
export_str += " --aid \"\""
if aid_ref_empty_do is not None:
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

@@ -7,7 +7,7 @@ there are also automatic card feeders.
"""
#
# (C) 2019 by sysmocom - s.f.m.c. GmbH
# (C) 2019 by Sysmocom s.f.m.c. GmbH
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
@@ -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-2025 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,228 +29,95 @@ 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
from pySim.log import PySimLogger
import abc
import csv
import logging
import yaml
log = PySimLogger.get(__name__)
card_key_providers = [] # type: List['CardKeyProvider']
class CardKeyFieldCryptor:
"""
A Card key field encryption class that may be used by Card key provider implementations to add support for
a column-based encryption to protect sensitive material (cryptographic key material, ADM keys, etc.).
The sensitive material is encrypted using a "key-encryption key", occasionally also known as "transport key"
before it is stored into a file or database (see also GSMA FS.28). The "transport key" is then used to decrypt
the key material on demand.
"""
# 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_ISDA', 'SCP03_MAC_ISDA', 'SCP03_DEK_ISDA'],
'SCP03_ECASD': ['SCP03_ENC_ECASD', 'SCP03_MAC_ECASD', 'SCP03_DEK_ECASD'],
}
__IV = b'\x23' * 16
@staticmethod
def __dict_keys_to_upper(d: dict) -> dict:
return {k.upper():v for k,v in d.items()}
@staticmethod
def __process_transport_keys(transport_keys: dict, crypt_groups: 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 __init__(self, transport_keys: dict):
"""
Create new field encryptor/decryptor object and set transport keys, usually one for each column. In some cases
it is also possible to use a single key for multiple columns (see also __CRYPT_GROUPS)
Args:
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.transport_keys = self.__process_transport_keys(self.__dict_keys_to_upper(transport_keys),
self.__CRYPT_GROUPS)
for name, key in self.transport_keys.items():
log.debug("Encrypting/decrypting field %s using AES key %s" % (name, key))
def decrypt_field(self, field_name: str, encrypted_val: str) -> str:
"""
Decrypt a single field. The decryption is only applied if we have a transport key is known under the provided
field name, otherwise the field is treated as plaintext and passed through as it is.
Args:
field_name : name of the field to decrypt (used to identify which key to use)
encrypted_val : encrypted field value
Returns:
plaintext field value
"""
if not field_name.upper() in self.transport_keys:
return encrypted_val
cipher = AES.new(h2b(self.transport_keys[field_name.upper()]), AES.MODE_CBC, self.__IV)
return b2h(cipher.decrypt(h2b(encrypted_val)))
def encrypt_field(self, field_name: str, plaintext_val: str) -> str:
"""
Encrypt a single field. The encryption is only applied if we have a transport key is known under the provided
field name, otherwise the field is treated as non sensitive and passed through as it is.
Args:
field_name : name of the field to decrypt (used to identify which key to use)
encrypted_val : encrypted field value
Returns:
plaintext field value
"""
if not field_name.upper() in self.transport_keys:
return plaintext_val
cipher = AES.new(h2b(self.transport_keys[field_name.upper()]), AES.MODE_CBC, self.__IV)
return b2h(cipher.encrypt(h2b(plaintext_val)))
class CardKeyProvider(abc.ABC):
"""Base class, not containing any concrete implementation."""
@abc.abstractmethod
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
"""
Get multiple card-individual fields for identified card. This method should not fail with an exception in
case the entry, columns or even the key column itsself is not found.
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]:
"""Verify multiple fields for identified card.
Args:
fields : list of valid field names such as 'ADM1', 'PIN1', ... which are to be obtained
key : look-up key to identify card data, such as 'ICCID'
value : value for look-up key to identify card data
Returns:
dictionary of {field : value, ...} strings for each requested field from 'fields'. In case nothing is
fond None shall be returned.
dictionary of {field, value} strings for each requested field from 'fields'
"""
for f in fields:
if (f not in self.VALID_FIELD_NAMES):
raise ValueError("Requested field name '%s' is not a valid field name, valid field names are: %s" %
(f, str(self.VALID_FIELD_NAMES)))
if (key not in self.VALID_FIELD_NAMES):
raise ValueError("Key field name '%s' is not a valid field name, valid field names are: %s" %
(key, str(self.VALID_FIELD_NAMES)))
return {}
def get_field(self, field: str, key: str = 'ICCID', value: str = "") -> Optional[str]:
"""get a single field from CSV file using a specified key/value pair"""
fields = [field]
result = self.get(fields, key, value)
return result.get(field)
@abc.abstractmethod
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
"""Get multiple card-individual fields for identified card.
Args:
fields : list of valid field names such as 'ADM1', 'PIN1', ... which are to be obtained
key : look-up key to identify card data, such as 'ICCID'
value : value for look-up key to identify card data
Returns:
dictionary of {field, value} strings for each requested field from 'fields'
"""
def __str__(self):
return type(self).__name__
class CardKeyProviderCsv(CardKeyProvider):
"""Card key provider implementation that allows to query against a specified CSV file."""
"""Card key provider implementation that allows to query against a specified CSV file"""
csv_file = None
filename = None
def __init__(self, csv_filename: str, transport_keys: dict):
def __init__(self, filename: str):
"""
Args:
csv_filename : file name (path) of CSV file containing card-individual key/data
transport_keys : (see class CardKeyFieldCryptor)
filename : file name (path) of CSV file containing card-individual key/data
"""
log.info("Using CSV file as card key data source: %s" % csv_filename)
self.csv_file = open(csv_filename, 'r')
self.csv_file = open(filename, 'r')
if not self.csv_file:
raise RuntimeError("Could not open CSV file '%s'" % csv_filename)
self.csv_filename = csv_filename
self.crypt = CardKeyFieldCryptor(transport_keys)
raise RuntimeError("Could not open CSV file '%s'" % filename)
self.filename = filename
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
super()._verify_get_data(fields, key, value)
self.csv_file.seek(0)
cr = csv.DictReader(self.csv_file)
if not cr:
raise RuntimeError("Could not open DictReader for CSV-File '%s'" % self.csv_filename)
raise RuntimeError(
"Could not open DictReader for CSV-File '%s'" % self.filename)
cr.fieldnames = [field.upper() for field in cr.fieldnames]
if key not in cr.fieldnames:
return None
return_dict = {}
rc = {}
for row in cr:
if row[key] == value:
for f in fields:
if f in row:
return_dict.update({f: self.crypt.decrypt_field(f, row[f])})
rc.update({f: row[f]})
else:
raise RuntimeError("CSV-File '%s' lacks column '%s'" % (self.csv_filename, f))
if return_dict == {}:
return None
return return_dict
class CardKeyProviderPgsql(CardKeyProvider):
"""Card key provider implementation that allows to query against a specified PostgreSQL database table."""
def __init__(self, config_filename: str, transport_keys: dict):
"""
Args:
config_filename : file name (path) of CSV file containing card-individual key/data
transport_keys : (see class CardKeyFieldCryptor)
"""
import psycopg2
log.info("Using SQL database as card key data source: %s" % config_filename)
with open(config_filename, "r") as cfg:
config = yaml.load(cfg, Loader=yaml.FullLoader)
log.info("Card key database name: %s" % config.get('db_name'))
db_users = config.get('db_users')
user = db_users.get('reader')
if user is None:
raise ValueError("user for role 'reader' not set up in config file.")
self.conn = psycopg2.connect(dbname=config.get('db_name'),
user=user.get('name'),
password=user.get('pass'),
host=config.get('host'))
self.tables = config.get('table_names')
log.info("Card key database tables: %s" % str(self.tables))
self.crypt = CardKeyFieldCryptor(transport_keys)
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
import psycopg2
from psycopg2.sql import Identifier, SQL
db_result = None
for t in self.tables:
self.conn.rollback()
cur = self.conn.cursor()
# Make sure that the database table and the key column actually exists. If not, move on to the next table
cur.execute("SELECT column_name FROM information_schema.columns where table_name = %s;", (t,))
cols_result = cur.fetchall()
if cols_result == []:
log.warning("Card Key database seems to lack table %s, check config file!" % t)
continue
if (key.lower(),) not in cols_result:
continue
# Query requested columns from database table
query = SQL("SELECT {}").format(Identifier(fields[0].lower()))
for f in fields[1:]:
query += SQL(", {}").format(Identifier(f.lower()))
query += SQL(" FROM {} WHERE {} = %s LIMIT 1;").format(Identifier(t.lower()),
Identifier(key.lower()))
cur.execute(query, (value,))
db_result = cur.fetchone()
cur.close()
if db_result:
break
if db_result is None:
return None
result = dict(zip(fields, db_result))
for k in result.keys():
result[k] = self.crypt.decrypt_field(k, result.get(k))
return result
raise RuntimeError("CSV-File '%s' lacks column '%s'" %
(self.filename, f))
return rc
def card_key_provider_register(provider: CardKeyProvider, provider_list=card_key_providers):
@@ -261,11 +128,11 @@ def card_key_provider_register(provider: CardKeyProvider, provider_list=card_key
provider_list : override the list of providers from the global default
"""
if not isinstance(provider, CardKeyProvider):
raise ValueError("provider is not a card data provider")
raise ValueError("provider is not a card data provier")
provider_list.append(provider)
def card_key_provider_get(fields: list[str], key: str, value: str, provider_list=card_key_providers) -> Dict[str, str]:
def card_key_provider_get(fields, key: str, value: str, provider_list=card_key_providers) -> Dict[str, str]:
"""Query all registered card data providers for card-individual [key] data.
Args:
@@ -276,21 +143,17 @@ def card_key_provider_get(fields: list[str], key: str, value: str, provider_list
Returns:
dictionary of {field, value} strings for each requested field from 'fields'
"""
key = key.upper()
fields = [f.upper() for f in fields]
for p in provider_list:
if not isinstance(p, CardKeyProvider):
raise ValueError("Provider list contains element which is not a card data provider")
log.debug("Searching for card key data (key=%s, value=%s, provider=%s)" % (key, value, str(p)))
raise ValueError(
"provider list contains element which is not a card data provier")
result = p.get(fields, key, value)
if result:
log.debug("Found card data: %s" % (str(result)))
return result
raise ValueError("Unable to find card key data (key=%s, value=%s, fields=%s)" % (key, value, str(fields)))
return {}
def card_key_provider_get_field(field: str, key: str, value: str, provider_list=card_key_providers) -> str:
def card_key_provider_get_field(field: str, key: str, value: str, provider_list=card_key_providers) -> Optional[str]:
"""Query all registered card data providers for a single field.
Args:
@@ -301,7 +164,11 @@ def card_key_provider_get_field(field: str, key: str, value: str, provider_list=
Returns:
dictionary of {field, value} strings for the requested field
"""
fields = [field]
result = card_key_provider_get(fields, key, value, card_key_providers)
return result.get(field.upper())
for p in provider_list:
if not isinstance(p, CardKeyProvider):
raise ValueError(
"provider list contains element which is not a card data provier")
result = p.get_field(field, key, value)
if result:
return result
return None

View File

@@ -22,12 +22,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Optional, Tuple
from osmocom.utils import *
from pySim.ts_102_221 import EF_DIR, CardProfileUICC
from typing import Optional, Dict, Tuple
from pySim.ts_102_221 import EF_DIR
from pySim.ts_51_011 import DF_GSM
from pySim.utils import SwHexstr
import abc
from pySim.utils import *
from pySim.commands import Path, SimCardCommands
class CardBase:
@@ -40,7 +40,8 @@ class CardBase:
rc = self._scc.reset_card()
if rc == 1:
return self._scc.get_atr()
return None
else:
return None
def set_apdu_parameter(self, cla: Hexstr, sel_ctrl: Hexstr) -> None:
"""Set apdu parameters (class byte and selection control bytes)"""
@@ -53,19 +54,13 @@ class CardBase:
def erase(self):
print("warning: erasing is not supported for specified card type!")
return
def file_exists(self, fid: Path) -> bool:
"""Determine if the file exists (and is not deactivated)."""
res_arr = self._scc.try_select_path(fid)
for res in res_arr:
if res[1] != '9000':
return False
try:
d = CardProfileUICC.decode_select_response(res_arr[-1][0])
if d.get('life_cycle_status_integer', 'operational_activated') != 'operational_activated':
return False
except:
pass
return True
def read_aids(self) -> List[Hexstr]:
@@ -73,16 +68,6 @@ class CardBase:
# callers having to do hasattr('read_aids') ahead of every call.
return []
def adf_present(self, adf: str = "usim") -> bool:
# a non-UICC doesn't have any applications. Convenience helper to avoid
# callers having to do hasattr('adf_present') ahead of every call.
return False
def select_adf_by_aid(self, adf: str = "usim", scc: Optional[SimCardCommands] = None) -> Tuple[Optional[Hexstr], Optional[SwHexstr]]:
# a non-UICC doesn't have any applications. Convenience helper to avoid
# callers having to do hasattr('select_adf_by_aid') ahead of every call.
return (None, None)
class SimCardBase(CardBase):
"""Here we only add methods for commands specified in TS 51.011, without
@@ -90,7 +75,7 @@ class SimCardBase(CardBase):
name = 'SIM'
def __init__(self, scc: SimCardCommands):
super().__init__(scc)
super(SimCardBase, self).__init__(scc)
self._scc.cla_byte = "A0"
self._scc.sel_ctrl = "0000"
@@ -103,7 +88,7 @@ class UiccCardBase(SimCardBase):
name = 'UICC'
def __init__(self, scc: SimCardCommands):
super().__init__(scc)
super(UiccCardBase, self).__init__(scc)
self._scc.cla_byte = "00"
self._scc.sel_ctrl = "0004" # request an FCP
# See also: ETSI TS 102 221, Table 9.3
@@ -112,8 +97,6 @@ class UiccCardBase(SimCardBase):
def probe(self) -> bool:
# EF.DIR is a mandatory EF on all ICCIDs; however it *may* also exist on a TS 51.011 SIM
ef_dir = EF_DIR()
# select MF first
self.file_exists("3f00")
return self.file_exists(ef_dir.fid)
def read_aids(self) -> List[Hexstr]:
@@ -175,8 +158,9 @@ class UiccCardBase(SimCardBase):
aid_full = self._complete_aid(aid)
if aid_full:
return scc.select_adf(aid_full)
# If we cannot get the full AID, try with short AID
return scc.select_adf(aid)
else:
# If we cannot get the full AID, try with short AID
return scc.select_adf(aid)
return (None, None)
def card_detect(scc: SimCardCommands) -> Optional[CardBase]:

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, BitsInteger
from construct import Struct, Enum, BitStruct, this
from construct import Switch, GreedyRange, FlagsEnum
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
from osmocom.utils import b2h, h2b
from pySim.utils import dec_xplmn_w_act
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 PlmnAdapter, 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
# Tag values as per TS 101 220 Table 7.23
# TS 102 223 Section 8.1
class Address(COMPR_TLV_IE, tag=0x86):
_construct = Struct('ton_npi'/TonNpi,
'call_number'/BcdAdapter(GreedyBytes))
class Address(COMPR_TLV_IE, tag=0x06):
_construct = Struct('ton_npi'/Int8ub,
'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,
@@ -255,27 +240,24 @@ class Result(COMPR_TLV_IE, tag=0x83):
'launch_browser_generic_error': AddlInfoLaunchBrowser,
'bearer_independent_protocol_error': AddlInfoBip,
'frames_error': AddlInfoFrames
}, default=GreedyBytes))
}, default=HexAdapter(GreedyBytes)))
# TS 102 223 Section 8.13 + TS 31.111 Section 8.13
class SMS_TPDU(COMPR_TLV_IE, tag=0x8B):
_construct = Struct('tpdu'/GreedyBytes)
_construct = Struct('tpdu'/HexAdapter(GreedyBytes))
# TS 31.111 Section 8.14
class SsString(COMPR_TLV_IE, tag=0x89):
_construct = Struct('ton_npi'/TonNpi, 'ss_string'/GreedyBytes)
_construct = Struct('ton_npi'/TonNpi, 'ss_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.15
class TextString(COMPR_TLV_IE, tag=0x8D):
_test_de_encode = [
( '8d090470617373776f7264', {'dcs': 4, 'text_string': b'password'} )
]
class TextString(COMPR_TLV_IE, tag=0x0d):
_construct = Struct('dcs'/Int8ub, # TS 03.38
'text_string'/GreedyBytes)
'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,39 +288,39 @@ 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'/GreedyBytes)
'ussd_string'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.18
class FileList(COMPR_TLV_IE, tag=0x92):
FileId=Bytes(2)
class FileList(COMPR_TLV_IE, tag=0x12):
FileId=HexAdapter(Bytes(2))
_construct = Struct('number_of_files'/Int8ub,
'files'/GreedyRange(FileId))
# TS 102 223 Section 8.19
# TS 102 223 Secton 8.19
class LocationInformation(COMPR_TLV_IE, tag=0x93):
pass
# TS 102 223 Section 8.20
# TS 102 223 Secton 8.20
class IMEI(COMPR_TLV_IE, tag=0x94):
_construct = BcdAdapter(GreedyBytes)
# TS 102 223 Section 8.21
# TS 102 223 Secton 8.21
class HelpRequest(COMPR_TLV_IE, tag=0x95):
pass
# TS 102 223 Section 8.22
# TS 102 223 Secton 8.22
class NetworkMeasurementResults(COMPR_TLV_IE, tag=0x96):
_construct = BcdAdapter(GreedyBytes)
# TS 102 223 Section 8.23
class DefaultText(COMPR_TLV_IE, tag=0x97):
_construct = Struct('dcs'/Int8ub,
'text_string'/GreedyBytes)
'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)
@@ -394,7 +376,7 @@ class ItemIconIdentifierList(COMPR_TLV_IE, tag=0x9f):
# TS 102 223 Section 8.35
class CApdu(COMPR_TLV_IE, tag=0xA2):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.37
class TimerIdentifier(COMPR_TLV_IE, tag=0xA4):
@@ -406,51 +388,28 @@ class TimerValue(COMPR_TLV_IE, tag=0xA5):
# TS 102 223 Section 8.40
class AtCommand(COMPR_TLV_IE, tag=0xA8):
_construct = GreedyBytes
_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 = GreedyBytes
_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 = 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': b'', 'bearer_type': 'default'} ),
]
# TS 31.111 Section 8.52.1
BearerParsCs = Struct('data_rate'/Int8ub,
'bearer_service'/Int8ub,
@@ -492,11 +451,11 @@ class BearerDescription(COMPR_TLV_IE, tag=0xB5):
'packet_grps_utran_eutran': BearerParsPacket,
'packet_with_extd_params': BearerParsPacketExt,
'ng_ran': BearerParsNgRan,
}, default=GreedyBytes))
}, default=HexAdapter(GreedyBytes)))
# TS 102 223 Section 8.53
class ChannelData(COMPR_TLV_IE, tag = 0xB6):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.54
class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
@@ -506,33 +465,26 @@ 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 = GreedyBytes
pass
# TS 102 223 Section 8.58
class OtherAddress(COMPR_TLV_IE, tag = 0xBE):
_test_de_encode = [
( 'be052101020304', {'address': h2b('01020304'), 'type_of_address': 'ipv4'} ),
]
_construct = Struct('type_of_address'/Enum(Int8ub, ipv4=0x21, ipv6=0x57),
'address'/GreedyBytes)
'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):
_construct = Struct('aid'/GreedyBytes)
class Aid(COMPR_TLV_IE, tag=0x2f):
_construct = Struct('aid'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.61
class AccessTechnology(COMPR_TLV_IE, tag=0xBF):
@@ -546,38 +498,35 @@ class ServiceRecord(COMPR_TLV_IE, tag=0xC1):
BearerTechId = Enum(Int8ub, technology_independent=0, bluetooth=1, irda=2, rs232=3, usb=4)
_construct = Struct('local_bearer_technology'/BearerTechId,
'service_identifier'/Int8ub,
'service_record'/GreedyBytes)
'service_record'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.64
class DeviceFilter(COMPR_TLV_IE, tag=0xC2):
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
'device_filter'/GreedyBytes)
'device_filter'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.65
class ServiceSearchIE(COMPR_TLV_IE, tag=0xC3):
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
'service_search'/GreedyBytes)
'service_search'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.66
class AttributeInformation(COMPR_TLV_IE, tag=0xC4):
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
'attribute_information'/GreedyBytes)
'attribute_information'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.68
class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
_construct = Struct('coding_type'/Enum(Int8ub, ieee802_16=0, irda=1),
'address'/GreedyBytes)
'address'/HexAdapter(GreedyBytes))
# TS 102 223 Section 8.70
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
_test_de_encode = [
( 'c704036e6161', h2b('036e6161') ),
]
_construct = GreedyBytes
_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,20 +562,20 @@ 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
class MultimediaMessageReference(COMPR_TLV_IE, tag=0xEA):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.83
class MultimediaMessageIdentifier(COMPR_TLV_IE, tag=0xEB):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.85
class MmContentIdentifier(COMPR_TLV_IE, tag=0xEE):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.89
class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
@@ -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
@@ -649,7 +598,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)),
'lac'/Bytes(2),
'lac'/HexAdapter(Bytes(2)),
'rac'/Int8ub)
# TS 31.111 Section 8.92
@@ -709,23 +658,23 @@ class EcatSequenceNumber(COMPR_TLV_IE, tag=0xA1):
# TS 102 223 Section 8.99
class EncryptedTlvList(COMPR_TLV_IE, tag=0xA2):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.100
class Mac(COMPR_TLV_IE, tag=0xE0):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.101
class SaTemplate(COMPR_TLV_IE, tag=0xA3):
_construct = GreedyBytes
_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
class DnsServerAddress(COMPR_TLV_IE, tag=0xC0):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# TS 102 223 Section 8.105
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
@@ -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

@@ -19,15 +19,15 @@ 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.utils import *
from pySim.filesystem import *
from pySim.profile import match_ruim
from pySim.profile import CardProfile, CardProfileAddon
from pySim.ts_51_011 import CardProfileSIM
from pySim.ts_51_011 import DF_TELECOM, DF_GSM
from pySim.ts_51_011 import EF_ServiceTable
from pySim.construct import *
from construct import *
# Mapping between CDMA Service Number and its description
@@ -115,7 +115,7 @@ class EF_AD(TransparentEF):
'''3.4.33 Administrative Data'''
_test_de_encode = [
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : b'\x00\x00', 'rfu' : b'' } ),
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : '' } ),
]
_test_no_pad = True
@@ -134,9 +134,9 @@ class EF_AD(TransparentEF):
# Byte 1: Display Condition
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
# Bytes 2-3: Additional information
'additional_info'/Bytes(2),
'additional_info'/HexAdapter(Bytes(2)),
# Bytes 4..: RFU
'rfu'/GreedyBytesRFU,
'rfu'/HexAdapter(GreedyBytesRFU),
)
@@ -178,23 +178,20 @@ class DF_CDMA(CardDF):
class CardProfileRUIM(CardProfile):
'''R-UIM card profile as per 3GPP2 C.S0023-D'''
ORDER = 20
ORDER = 2
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:
def decode_select_response(resp_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"])
return CardProfileSIM.decode_select_response(resp_hex)
@staticmethod
def match_with_card(scc: SimCardCommands) -> bool:
return match_ruim(scc)
class AddonRUIM(CardProfileAddon):
"""An Addon that can be found on on a combined SIM + RUIM or UICC + RUIM to support CDMA."""

View File

@@ -21,15 +21,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import List, Tuple
from typing import List, Optional, 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, filter_dict
from pySim.utils import rpad, lpad, b2h, h2b, sw_match, bertlv_encode_len, Hexstr, h2i, i2h, str_sanitize, expand_hex, SwMatchstr
from pySim.utils import Hexstr, SwHexstr, ResTuple
from pySim.exceptions import SwMatchError
from pySim.transport import LinkBase
@@ -66,6 +64,7 @@ class SimCardCommands:
byte by the respective instance. """
def __init__(self, transport: LinkBase, lchan_nr: int = 0):
self._tp = transport
self._cla_byte = None
self.sel_ctrl = "0000"
self.lchan_nr = lchan_nr
# invokes the setter below
@@ -75,58 +74,67 @@ class SimCardCommands:
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.cla_byte = self._cla_byte
ret.sel_ctrl = self.sel_ctrl
return ret
@property
def max_cmd_len(self) -> int:
"""Maximum length of the command apdu data section. Depends on secure channel protocol used."""
if self.scp:
return 255 - self.scp.overhead
else:
return 255
def cla_byte(self) -> Hexstr:
"""Return the (cached) patched default CLA byte for this card."""
return self._cla4lchan
def send_apdu(self, pdu: Hexstr, apply_lchan:bool = True) -> ResTuple:
@cla_byte.setter
def cla_byte(self, new_val: Hexstr):
"""Set the (raw, without lchan) default CLA value for this card."""
self._cla_byte = new_val
# compute cached result
self._cla4lchan = cla_with_lchan(self._cla_byte, self.lchan_nr)
def cla4lchan(self, cla: Hexstr) -> Hexstr:
"""Compute the lchan-patched value of the given CLA value. If no CLA
value is provided as argument, the lchan-patched version of the SimCardCommands._cla_byte
value is used. Most commands will use the latter, while some wish to override it and
can pass it as argument here."""
if not cla:
# return cached result to avoid re-computing this over and over again
return self._cla4lchan
else:
return cla_with_lchan(cla, self.lchan_nr)
def send_apdu(self, pdu: Hexstr) -> ResTuple:
"""Sends an APDU and auto fetch response data
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
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:
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""Sends an APDU and check returned SW
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
digits using a '?' to add some ambiguity if needed.
apply_lchan : apply the currently selected lchan to the CLA byte before sending
digits using a '?' to add some ambiguity if needed.
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if 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]:
cmd_data: Hexstr, resp_constr: Construct) -> Tuple[dict, SwHexstr]:
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
@@ -136,16 +144,14 @@ class SimCardCommands:
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
resp_cosntr : defining how to decode binary APDU response data
Returns:
Tuple of (decoded_data, sw)
"""
cmd = cmd_constr.build(cmd_data) if cmd_data else b''
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)
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
p3 = i2h([len(cmd)])
pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)])
(data, sw) = self.send_apdu(pdu)
if data:
# filter the resulting dict to avoid '_io' members inside
rsp = filter_dict(resp_constr.parse(h2b(data)))
@@ -155,7 +161,7 @@ class SimCardCommands:
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]:
sw_exp: SwMatchstr="9000") -> Tuple[dict, SwHexstr]:
"""Build and sends an APDU using a 'construct' definition; parses response.
Args:
@@ -170,8 +176,8 @@ class SimCardCommands:
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)
(rsp, sw) = self.send_apdu_constr(cla, ins,
p1, p2, cmd_constr, cmd_data, resp_constr)
if not sw_match(sw, sw_exp):
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
return (rsp, sw)
@@ -196,7 +202,6 @@ class SimCardCommands:
# checking if the length of the remaining TLV string matches
# what we get in the length field.
# See also ETSI TS 102 221, chapter 11.1.1.3.0 Base coding.
# TODO: this likely just is normal BER-TLV ("All data objects are BER-TLV except if otherwise # defined.")
exp_tlv_len = int(fcp[2:4], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 4
@@ -204,7 +209,6 @@ class SimCardCommands:
exp_tlv_len = int(fcp[2:6], 16)
if len(fcp[4:]) // 2 == exp_tlv_len:
skip = 6
raise ValueError('Cannot determine length of TLV-length')
# Skip FCP tag and length
tlv = fcp[skip:]
@@ -245,10 +249,10 @@ 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.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
rv.append((data, sw))
if sw != '9000':
return rv
@@ -264,10 +268,10 @@ 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
@@ -278,21 +282,21 @@ class SimCardCommands:
fid : file identifier as hex string
"""
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid + "00")
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
def select_parent_df(self) -> ResTuple:
"""Execute SELECT to switch to the parent DF """
return self.send_apdu_checksw(self.cla_byte + "a40304")
return self.send_apdu_checksw(self.cla_byte + "a4030400")
def select_adf(self, aid: Hexstr) -> ResTuple:
"""Execute SELECT a given Application ADF.
"""Execute SELECT a given Applicaiton ADF.
Args:
aid : application identifier as hex string
"""
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid + "00")
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
"""Execute READD BINARY.
@@ -313,14 +317,14 @@ 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)
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
@@ -371,7 +375,7 @@ class SimCardCommands:
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) + \
@@ -379,8 +383,8 @@ class SimCardCommands:
try:
chunk_data, chunk_sw = self.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:
@@ -436,10 +440,10 @@ 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:
elif (len(data) // 2 < rec_length):
if leftpad:
data = lpad(data, rec_length * 2)
else:
@@ -495,9 +499,9 @@ class SimCardCommands:
# TS 102 221 Section 11.3.1 low-level helper
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
if first:
pdu = '80cb008001%02x00' % (tag)
pdu = self.cla4lchan('80') + 'cb008001%02x' % (tag)
else:
pdu = '80cb0000'
pdu = self.cla4lchan('80') + 'cb000000'
return self.send_apdu_checksw(pdu)
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
@@ -514,7 +518,7 @@ 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
@@ -525,9 +529,9 @@ class SimCardCommands:
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)
pdu = self.cla4lchan('80') + 'db00%02x%02x%s' % (p1, len(data)//2, data)
return self.send_apdu_checksw(pdu)
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
@@ -555,10 +559,10 @@ 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:
@@ -570,20 +574,21 @@ 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.send_apdu_checksw(self.cla4lchan('a0') + '88000010' + rand, sw='9000')
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
"""Execute AUTHENTICATE (USIM/ISIM).
Args:
rand : 16 byte random data as hex string (RAND)
autn : 8 byte Authentication Token (AUTN)
autn : 8 byte Autentication Token (AUTN)
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}
@@ -591,8 +596,6 @@ class SimCardCommands:
p2 = '81'
elif context == 'gsm':
p2 = '80'
else:
raise ValueError("Unsupported context '%s'" % context)
(data, sw) = self.send_apdu_constr_checksw(
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
if 'auts' in data:
@@ -603,7 +606,7 @@ class SimCardCommands:
def status(self) -> ResTuple:
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
return self.send_apdu_checksw('80F20000')
return self.send_apdu_checksw(self.cla4lchan('80') + 'F20000ff')
def deactivate_file(self) -> ResTuple:
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
@@ -618,12 +621,12 @@ class SimCardCommands:
return self.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"""
"""Execute CREEATE 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 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))
return self.send_apdu_checksw(self.cla4lchan('80') + 'd40000%02x%s' % (len(payload)//2, payload))
def delete_file(self, fid: Hexstr) -> ResTuple:
"""Execute DELETE FILE command as per TS 102 222 Section 6.4"""
@@ -652,7 +655,7 @@ class SimCardCommands:
p1 = 0x80
else:
p1 = 0x00
pdu = self.cla_byte + '70%02x%02x' % (p1, lchan_nr)
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
return self.send_apdu_checksw(pdu)
def reset_card(self) -> Hexstr:
@@ -663,8 +666,8 @@ class SimCardCommands:
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:
"""Verify a given CHV (Card Holder Verification == PIN)
@@ -735,7 +738,7 @@ class SimCardCommands:
Args:
payload : payload as hex string
"""
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload) + "00", apply_lchan = False)
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
def terminal_profile(self, payload: Hexstr) -> ResTuple:
"""Send TERMINAL PROFILE to card
@@ -744,7 +747,7 @@ class SimCardCommands:
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.send_apdu(('80100000%02x' % data_length) + payload)
return (data, sw)
# ETSI TS 102 221 11.1.22
@@ -758,31 +761,33 @@ 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]
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.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)
@@ -792,12 +797,11 @@ class SimCardCommands:
"""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)
data, sw = self.send_apdu_checksw('8076010008' + token)
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))
data, sw = self.send_apdu('%02xca%04x00' % (cla, tag))
return (data, sw)
# TS 31.102 Section 7.5.2

552
pySim/construct.py Normal file
View File

@@ -0,0 +1,552 @@
from construct.lib.containers import Container, ListContainer
from construct.core import EnumIntegerString
import typing
from construct import *
from construct.core import evaluate, BitwisableString
from construct.lib import integertypes
from pySim.utils import b2h, h2b, swap_nibbles
import gsm0338
import codecs
import ipaddress
"""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 Utf8Adapter(Adapter):
"""convert a bytes() type that contains utf8 encoded text to human readable text."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
return codecs.decode(obj, "utf-8")
def _encode(self, obj, context, path):
return codecs.encode(obj, "utf-8")
class GsmOrUcs2Adapter(Adapter):
"""Try to encode into a GSM 03.38 string; if that fails, fall back to UCS-2 as described
in TS 102 221 Annex A."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
# one of the magic bytes of TS 102 221 Annex A
if obj[0] in [0x80, 0x81, 0x82]:
ad = Ucs2Adapter(GreedyBytes)
else:
ad = GsmString(GreedyBytes)
return ad._decode(obj, context, path)
def _encode(self, obj, context, path):
# first try GSM 03.38; then fall back to TS 102 221 Annex A UCS-2
try:
ad = GsmString(GreedyBytes)
return ad._encode(obj, context, path)
except:
ad = Ucs2Adapter(GreedyBytes)
return ad._encode(obj, context, path)
class Ucs2Adapter(Adapter):
"""convert a bytes() type that contains UCS2 encoded characters encoded as defined in TS 102 221
Annex A to normal python string representation (and back)."""
def _decode(self, obj, context, path):
# In case the string contains only 0xff bytes we interpret it as an empty string
if obj == b'\xff' * len(obj):
return ""
if obj[0] == 0x80:
# TS 102 221 Annex A Variant 1
return codecs.decode(obj[1:], 'utf_16_be')
elif obj[0] == 0x81:
# TS 102 221 Annex A Variant 2
out = ""
# second byte contains a value indicating the number of characters
num_of_chars = obj[1]
# the third byte contains an 8 bit number which defines bits 15 to 8 of a 16 bit base
# pointer, where bit 16 is set to zero, and bits 7 to 1 are also set to zero. These
# sixteen bits constitute a base pointer to a "half-page" in the UCS2 code space
base_ptr = obj[2] << 7
for ch in obj[3:3+num_of_chars]:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a
# GSM Default Alphabet character, whereas if bit 8 of the byte is set to one, then
# the remaining seven bits are an offset value added to the 16 bit base pointer
# defined earlier, and the resultant 16 bit value is a UCS2 code point
if ch & 0x80:
codepoint = (ch & 0x7f) + base_ptr
out += codecs.decode(codepoint.to_bytes(2, byteorder='big'), 'utf_16_be')
else:
out += codecs.decode(bytes([ch]), 'gsm03.38')
return out
elif obj[0] == 0x82:
# TS 102 221 Annex A Variant 3
out = ""
# second byte contains a value indicating the number of characters
num_of_chars = obj[1]
# third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
# pointer to a half-page in the UCS2 code space, for use with some or all of the
# remaining bytes in the string
base_ptr = obj[2] << 8 | obj[3]
for ch in obj[4:4+num_of_chars]:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a
# GSM Default Alphabet character, whereas if bit 8 of the byte is set to one, the
# remaining seven bits are an offset value added to the base pointer defined in
# bytes three and four, and the resultant 16 bit value is a UCS2 code point, else: #
# GSM default alphabet
if ch & 0x80:
codepoint = (ch & 0x7f) + base_ptr
out += codecs.decode(codepoint.to_bytes(2, byteorder='big'), 'utf_16_be')
else:
out += codecs.decode(bytes([ch]), 'gsm03.38')
return out
else:
raise ValueError('First byte of TS 102 221 UCS-2 must be 0x80, 0x81 or 0x82')
def _encode(self, obj, context, path):
def encodable_in_gsm338(instr: str) -> bool:
"""Determine if given input string is encode-ale in gsm03.38."""
try:
# TODO: figure out if/how we can constrain to default alphabet. The gsm0338
# library seems to include the spanish lock/shift table
codecs.encode(instr, 'gsm03.38')
except ValueError:
return False
return True
def codepoints_not_in_gsm338(instr: str) -> typing.List[int]:
"""Return an integer list of UCS2 codepoints for all characters of 'inster'
which are not representable in the GSM 03.38 default alphabet."""
codepoint_list = []
for c in instr:
if encodable_in_gsm338(c):
continue
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
codepoint_list.append(c_codepoint)
return codepoint_list
def diff_between_min_and_max_of_list(inlst: typing.List) -> int:
return max(inlst) - min(inlst)
def encodable_in_variant2(instr: str) -> bool:
codepoint_prefix = None
for c in instr:
if encodable_in_gsm338(c):
continue
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
if c_codepoint >= 0x8000:
return False
c_prefix = c_codepoint >> 7
if codepoint_prefix is None:
codepoint_prefix = c_prefix
else:
if c_prefix != codepoint_prefix:
return False
return True
def encodable_in_variant3(instr: str) -> bool:
codepoint_list = codepoints_not_in_gsm338(instr)
# compute delta between max and min; check if it's encodable in 7 bits
if diff_between_min_and_max_of_list(codepoint_list) >= 0x80:
return False
return True
def _encode_variant1(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 1"""
return b'\x80' + codecs.encode(obj, 'utf_16_be')
def _encode_variant2(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 2"""
codepoint_prefix = None
# second byte contains a value indicating the number of characters
hdr = b'\x81' + len(instr).to_bytes(1, byteorder='big')
chars = b''
for c in instr:
try:
enc = codecs.encode(c, 'gsm03.38')
except ValueError:
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
c_prefix = c_codepoint >> 7
if codepoint_prefix is None:
codepoint_prefix = c_prefix
assert codepoint_prefix == c_prefix
enc = (0x80 + (c_codepoint & 0x7f)).to_bytes(1, byteorder='big')
chars += enc
if codepoint_prefix == None:
codepoint_prefix = 0
return hdr + codepoint_prefix.to_bytes(1, byteorder='big') + chars
def _encode_variant3(instr: str) -> bytes:
"""Encode according to TS 102 221 Annex A Variant 3"""
# second byte contains a value indicating the number of characters
hdr = b'\x82' + len(instr).to_bytes(1, byteorder='big')
chars = b''
codepoint_list = codepoints_not_in_gsm338(instr)
codepoint_base = min(codepoint_list)
for c in instr:
try:
# if bit 8 of the byte is set to zero, the remaining 7 bits of the byte contain a GSM
# Default # Alphabet character
enc = codecs.encode(c, 'gsm03.38')
except ValueError:
# if bit 8 of the byte is set to one, the remaining seven bits are an offset
# value added to the base pointer defined in bytes three and four, and the
# resultant 16 bit value is a UCS2 code point
c_codepoint = int.from_bytes(codecs.encode(c, 'utf_16_be'), byteorder='big')
c_codepoint_delta = c_codepoint - codepoint_base
assert c_codepoint_delta < 0x80
enc = (0x80 + c_codepoint_delta).to_bytes(1, byteorder='big')
chars += enc
# third and fourth bytes contain a 16 bit number which defines the complete 16 bit base
# pointer to a half-page in the UCS2 code space
return hdr + codepoint_base.to_bytes(2, byteorder='big') + chars
if encodable_in_variant2(obj):
return _encode_variant2(obj)
elif encodable_in_variant3(obj):
return _encode_variant3(obj)
else:
return _encode_variant1(obj)
class BcdAdapter(Adapter):
"""convert a bytes() type to a string of BCD nibbles."""
def _decode(self, obj, context, path):
return swap_nibbles(b2h(obj))
def _encode(self, obj, context, path):
return h2b(swap_nibbles(obj))
class PlmnAdapter(BcdAdapter):
"""convert a bytes(3) type to BCD string like 262-02 or 262-002."""
def _decode(self, obj, context, path):
bcd = super()._decode(obj, context, path)
if bcd[3] == 'f':
return '-'.join([bcd[:3], bcd[4:]])
else:
return '-'.join([bcd[:3], bcd[3:]])
def _encode(self, obj, context, path):
l = obj.split('-')
if len(l[1]) == 2:
bcd = l[0] + 'f' + l[1]
else:
bcd = l[0] + l[1]
return super()._encode(bcd, context, path)
class InvertAdapter(Adapter):
"""inverse logic (false->true, true->false)."""
@staticmethod
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') or characters up to target size.
Decoder removes trailing padding bytes/characters.
Parameters:
subcon: Subconstruct as defined by construct library
pattern: set padding pattern (default: b'\\xff')
num_per_byte: number of 'elements' per byte. E.g. for hex nibbles: 2
"""
def __init__(self, subcon, pattern=b'\xff', num_per_byte=1):
super().__init__(subcon)
self.pattern = pattern
self.num_per_byte = num_per_byte
def _decode(self, obj, context, path):
return obj.rstrip(self.pattern)
def _encode(self, obj, context, path):
target_size = self.sizeof() * self.num_per_byte
if len(obj) > target_size:
raise SizeofError("Input ({}) exceeds target size ({})".format(
len(obj), target_size))
return obj + self.pattern * (target_size - len(obj))
class MultiplyAdapter(Adapter):
"""
Decoder multiplies by multiplicator
Encoder divides by multiplicator
Parameters:
subcon: Subconstruct as defined by construct library
multiplier: Multiplier to apply to raw encoded value
"""
def __init__(self, subcon, multiplicator):
super().__init__(subcon)
self.multiplicator = multiplicator
def _decode(self, obj, context, path):
return obj * 8
def _encode(self, obj, context, path):
return obj // 8
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)
class Ipv4Adapter(Adapter):
"""
Encoder converts from 4 bytes to string representation (A.B.C.D).
Decoder converts from string representation (A.B.C.D) to four bytes.
"""
def _decode(self, obj, context, path):
ia = ipaddress.IPv4Address(obj)
return ia.compressed
def _encode(self, obj, context, path):
ia = ipaddress.IPv4Address(obj)
return ia.packed
class Ipv6Adapter(Adapter):
"""
Encoder converts from 16 bytes to string representation.
Decoder converts from string representation to 16 bytes.
"""
def _decode(self, obj, context, path):
ia = ipaddress.IPv6Address(obj)
return ia.compressed
def _encode(self, obj, context, path):
ia = ipaddress.IPv6Address(obj)
return ia.packed
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 = '_', context: dict = {}):
"""Helper function to wrap around normalize_construct() and filter_dict()."""
if not length:
length = len(raw_bin_data)
try:
parsed = c.parse(raw_bin_data, total_len=length, **context)
except StreamError as e:
# if the input is all-ff, this means the content is undefined. Let's avoid passing StreamError
# exceptions in those situations (which might occur if a length field 0xff is 255 but then there's
# actually less bytes in the remainder of the file.
if all([v == 0xff for v in raw_bin_data]):
return None
else:
raise e
return normalize_construct(parsed)
def build_construct(c, decoded_data, context: dict = {}):
"""Helper function to handle total_len."""
return c.build(decoded_data, total_len=None, **context)
# here we collect some shared / common definitions of data types
LV = Prefixed(Int8ub, HexAdapter(GreedyBytes))
# 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')
def GsmOrUcs2String(n):
'''
GSM 03.38 or UCS-2 (TS 102 221 Annex A) encoded byte string of fixed length n.
Encoder appends padding bytes (b'\\xff') to maintain
length. Decoder removes those trailing bytes.
Exceptions are raised for invalid characters
and length excess.
Parameters:
n (Integer): Fixed length of the encoded byte string
'''
return GsmOrUcs2Adapter(Rpad(Bytes(n), pattern=b'\xff'))
class GreedyInteger(Construct):
"""A variable-length integer implementation, think of combining GrredyBytes with BytesInteger."""
def __init__(self, signed=False, swapped=False, minlen=0):
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 int.from_bytes(data, byteorder='big', signed=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 = obj.to_bytes(length, byteorder='big', signed=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,70 +1,24 @@
import sys
from typing import Optional, Tuple
from typing import Optional
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,
}
import asn1tools
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'):
def compile_asn1_subdir(subdir_name:str):
"""Helper function that compiles ASN.1 syntax from all files within given subdir"""
import asn1tools
asn_txt = ''
__ver = sys.version_info
if (__ver.major, __ver.minor) >= (3, 9):
for i in resources.files('pySim.esim').joinpath('asn1').joinpath(subdir_name).iterdir():
if not i.name.endswith('.asn'):
continue
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)
return asn1tools.compile_string(asn_txt, codec='der')
# SGP.22 section 4.1 Activation Code
class ActivationCode:
"""SGP.22 section 4.1 Activation Code"""
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')
@@ -80,7 +34,6 @@ class ActivationCode:
@staticmethod
def decode_str(ac: str) -> dict:
"""decode an activation code from its string representation."""
if ac[0] != '1':
raise ValueError("Unsupported AC_Format '%s'!" % ac[0])
ac_elements = ac.split('$')

View File

@@ -983,7 +983,7 @@ keyAccess [22] OCTET STRING (SIZE (1)) DEFAULT '00'H,
keyIdentifier [2] OCTET STRING (SIZE (1)),
keyVersionNumber [3] OCTET STRING (SIZE (1)),
keyCounterValue [5] OCTET STRING OPTIONAL,
keyComponents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
keyCompontents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
keyType [0] OCTET STRING,
keyData [6] OCTET STRING,
macLength[7] UInt8 DEFAULT 8

View File

@@ -1,9 +1,11 @@
"""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."""
# 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
@@ -21,6 +23,7 @@ Originally (SGP.22 v2.x) this was called SCP03t, but it has since been renamed t
# 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).
MAX_SEGMENT_SIZE = 1020
import abc
from typing import List
@@ -33,17 +36,13 @@ 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
from pySim.utils import bertlv_encode_len, bertlv_parse_one, b2h
# don't log by default
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
MAX_SEGMENT_SIZE = 1020
class BspAlgo(abc.ABC):
"""Base class representing a cryptographic algorithm within the BSP (BPP Security Protocol)."""
blocksize: int
def _get_padding(self, in_len: int, multiple: int, padding: int = 0) -> bytes:
@@ -51,17 +50,16 @@ class BspAlgo(abc.ABC):
if in_len % multiple == 0:
return b''
pad_cnt = multiple - (in_len % multiple)
return bytes([padding]) * pad_cnt
return b'\x00' * 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)
return indat + self._get_padding(len(indat), self.blocksize, padding)
def __str__(self):
return self.__class__.__name__
class BspAlgoCrypt(BspAlgo, abc.ABC):
"""Base class representing an encryption/decryption algorithm within the BSP (BPP Security Protocol)."""
def __init__(self, s_enc: bytes):
self.s_enc = s_enc
@@ -73,7 +71,7 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
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)[:20], b2h(data)[:20], b2h(padded_data)[:20], b2h(ciphertext)[:20])
block_nr, b2h(self.s_enc), b2h(data), b2h(padded_data), b2h(ciphertext))
return ciphertext
def decrypt(self, data:bytes) -> bytes:
@@ -83,17 +81,19 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
@abc.abstractmethod
def _unpad(self, padded: bytes) -> bytes:
"""Remove the padding from padded data."""
pass
@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
class BspAlgoCryptAES128(BspAlgoCrypt):
"""AES-CBC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
name = 'AES-CBC-128'
blocksize = 16
@@ -134,7 +134,6 @@ class BspAlgoCryptAES128(BspAlgoCrypt):
class BspAlgoMac(BspAlgo, abc.ABC):
"""Base class representing a message authentication code algorithm within the BSP (BPP Security Protocol)."""
l_mac = 0 # must be overridden by derived class
def __init__(self, s_mac: bytes, initial_mac_chaining_value: bytes):
@@ -149,20 +148,10 @@ class BspAlgoMac(BspAlgo, abc.ABC):
temp_data = self.mac_chain + tag_and_length + data
old_mcv = self.mac_chain
c_mac = self._auth(temp_data)
# DEBUG: Show MAC computation details
logger.debug(f"MAC_DEBUG: tag=0x{tag:02x}, lcc={lcc}")
logger.debug(f"MAC_DEBUG: tag_and_length: {tag_and_length.hex()}")
logger.debug(f"MAC_DEBUG: mac_chain[:20]: {old_mcv[:20].hex()}")
logger.debug(f"MAC_DEBUG: temp_data[:20]: {temp_data[:20].hex()}")
logger.debug(f"MAC_DEBUG: c_mac: {c_mac.hex()}")
# 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(f"MAC_DEBUG: final_output[:20]: {ret[:20].hex()}")
logger.debug("auth(tag=0x%x, mcv=%s, s_mac=%s, plaintext=%s, temp=%s) -> %s",
tag, b2h(old_mcv)[:20], b2h(self.s_mac)[:20], b2h(data)[:20], b2h(temp_data)[:20], b2h(ret)[:20])
tag, b2h(old_mcv), b2h(self.s_mac), b2h(data), b2h(temp_data), b2h(ret))
return ret
def verify(self, ciphertext: bytes) -> bool:
@@ -177,9 +166,9 @@ class BspAlgoMac(BspAlgo, abc.ABC):
@abc.abstractmethod
def _auth(self, temp_data: bytes) -> bytes:
"""To be implemented by algorithm specific derived class."""
pass
class BspAlgoMacAES128(BspAlgoMac):
"""AES-CMAC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
name = 'AES-CMAC-128'
l_mac = 8
@@ -214,11 +203,6 @@ def bsp_key_derivation(shared_secret: bytes, key_type: int, key_length: int, hos
s_enc = out[l:2*l]
s_mac = out[l*2:3*l]
logger.debug(f"BSP_KDF_DEBUG: kdf_out = {b2h(out)}")
logger.debug(f"BSP_KDF_DEBUG: initial_mcv = {b2h(initial_mac_chaining_value)}")
logger.debug(f"BSP_KDF_DEBUG: s_enc = {b2h(s_enc)}")
logger.debug(f"BSP_KDF_DEBUG: s_mac = {b2h(s_mac)}")
return s_enc, s_mac, initial_mac_chaining_value
@@ -243,24 +227,12 @@ class BspInstance:
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 ciphertext."""
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertex."""
assert tag <= 255
assert len(plaintext) <= self.max_payload_size
# DEBUG: Show what we're processing
logger.debug(f"BSP_DEBUG: encrypt_and_mac_one(tag=0x{tag:02x}, plaintext_len={len(plaintext)})")
logger.debug(f"BSP_DEBUG: plaintext[:20]: {plaintext[:20].hex()}")
logger.debug(f"BSP_DEBUG: s_enc[:20]: {self.c_algo.s_enc[:20].hex()}")
logger.debug(f"BSP_DEBUG: s_mac[:20]: {self.m_algo.s_mac[:20].hex()}")
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext)[:20])
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext))
ciphered = self.c_algo.encrypt(plaintext)
logger.debug(f"BSP_DEBUG: ciphered[:20]: {ciphered[:20].hex()}")
maced = self.m_algo.auth(tag, ciphered)
logger.debug(f"BSP_DEBUG: final_result[:20]: {maced[:20].hex()}")
logger.debug(f"BSP_DEBUG: final_result_len: {len(maced)}")
return maced
def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
@@ -280,11 +252,11 @@ class BspInstance:
return result
def mac_only_one(self, tag: int, plaintext: bytes) -> bytes:
"""MAC a single plaintext TLV. Returns the protected ciphertext."""
"""MAC a single plaintext TLV. Returns the protected ciphertex."""
assert tag <= 255
assert len(plaintext) <= self.max_payload_size
assert len(plaintext) < self.max_payload_size
maced = self.m_algo.auth(tag, plaintext)
# The data block counter for ICV calculation is incremented also for each segment with C-MAC only.
# 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
@@ -317,9 +289,7 @@ class BspInstance:
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 calculation is incremented also for each segment with C-MAC only.
self.c_algo.block_nr += 1
tdict, l, val, remain = bertlv_parse_one(payload)
return val
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:

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 18 to 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 (18, 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 (18, 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 class 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,5 +1,6 @@
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+ as per SGP22 v3.0 Section 5.5"""
# 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
@@ -16,17 +17,10 @@
# 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
from pySim.utils import b2h, h2b, bertlv_encode_tag, bertlv_encode_len
import pySim.esim.rsp as rsp
from pySim.esim.bsp import BspInstance
from pySim.esim import PMO
import logging
logger = logging.getLogger(__name__)
# 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
@@ -76,53 +70,18 @@ def gen_replace_session_keys(ppk_enc: bytes, ppk_cmac: bytes, initial_mcv: bytes
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, profile_class = 'operational'):
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.profile_class = profile_class
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) StoreMetadataRequest DO (SGP.22 5.5.3)"""
"""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.profile_class == 'test':
smr['profileClass'] = 0
elif self.profile_class == 'provisioning':
smr['profileClass'] = 1
elif self.profile_class == 'operational':
smr['profileClass'] = 2
else:
raise ValueError('Unsupported Profile Class %s' % self.profile_class)
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)
@@ -208,12 +167,8 @@ class BoundProfilePackage(ProfilePackage):
# 'initialiseSecureChannelRequest'
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
# firstSequenceOf87
logger.debug("BPP_ENCODE_DEBUG: Encrypting ConfigureISDP with BSP keys")
logger.debug(f"BPP_ENCODE_DEBUG: BSP S-ENC: {bsp.c_algo.s_enc.hex()}")
logger.debug(f"BPP_ENCODE_DEBUG: BSP S-MAC: {bsp.m_algo.s_mac.hex()}")
bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
# sequenceOF88
logger.debug("BPP_ENCODE_DEBUG: MAC-only StoreMetadata with BSP keys")
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
if self.ppp: # we have to use session keys
@@ -228,79 +183,3 @@ class BoundProfilePackage(ProfilePackage):
# 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 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
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,265 +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 representing a single parameter in the API."""
@classmethod
def verify_decoded(cls, data):
"""Verify the decoded representation of a value. Should raise an exception if something is odd."""
pass
@classmethod
def verify_encoded(cls, data):
"""Verify the encoded representation of a value. Should raise an exception if something 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)
if actual_sec:
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 class 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:
if response.headers.get('Content-Type').startswith('application/json'):
return self.decode(response.json())
elif response.headers.get('Content-Type').startswith('text/plain;charset=UTF-8'):
return { 'data': response.content.decode('utf-8') }
raise HttpHeaderError(f'unimplemented response Content-Type: {response.headers=!r}')
return None

View File

@@ -1,5 +1,6 @@
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning) as per SGP22 v3.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
@@ -18,12 +19,12 @@
from typing import Optional
import shelve
import copyreg
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 collections.abc import MutableMapping
from pySim.esim import compile_asn1_subdir
@@ -37,7 +38,7 @@ class RspSessionState:
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
# used at a later point between API calsl
self.ci_cert_id = ci_cert_id
self.euicc_cert: Optional[x509.Certificate] = None
self.eum_cert: Optional[x509.Certificate] = None
@@ -90,90 +91,11 @@ class RspSessionState:
# 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 remaining state
# automatically recover all the remainig state
self.__dict__.update(state)
class RspSessionStore:
"""A wrapper around the database-backed storage 'shelve' for storing RspSessionState objects.
Can be configured to use either file-based storage or in-memory storage.
We use it to store RspSessionState objects indexed by transactionId."""
def __init__(self, filename: Optional[str] = None, in_memory: bool = False):
self._in_memory = in_memory
if in_memory:
self._shelf = shelve.Shelf(dict())
else:
if filename is None:
raise ValueError("filename is required for file-based session store")
self._shelf = shelve.open(filename)
# dunder magic
def __getitem__(self, key):
return self._shelf[key]
def __setitem__(self, key, value):
self._shelf[key] = value
def __delitem__(self, key):
del self._shelf[key]
def __contains__(self, key):
return key in self._shelf
def __iter__(self):
return iter(self._shelf)
def __len__(self):
return len(self._shelf)
# everything else
def __getattr__(self, name):
"""Delegate attribute access to the underlying shelf object."""
return getattr(self._shelf, name)
def close(self):
"""Close the session store."""
if hasattr(self._shelf, 'close'):
self._shelf.close()
if self._in_memory:
# For in-memory store, clear the reference
self._shelf = None
def sync(self):
"""Synchronize the cache with the underlying storage."""
if hasattr(self._shelf, 'sync'):
self._shelf.sync()
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
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."""
pass

File diff suppressed because it is too large Load Diff

View File

@@ -1,357 +0,0 @@
"""Implementation of Personalization of eSIM profiles in SimAlliance/TCA Interoperable Profile:
Run a batch of N personalizations"""
# (C) 2025-2026 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
#
# Author: nhofmeyr@sysmocom.de
#
# 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 pprint
from typing import List, Generator
from pySim.esim.saip.personalization import ConfigurableParameter
from pySim.esim.saip import param_source
from pySim.esim.saip import ProfileElementSequence, ProfileElementSD
from pySim.global_platform import KeyUsageQualifier
from osmocom.utils import b2h
class BatchPersonalization:
"""Produce a series of eSIM profiles from predefined parameters.
Personalization parameters are derived from pysim.esim.saip.param_source.ParamSource.
Usage example:
der_input = some_file.open('rb').read()
pes = ProfileElementSequence.from_der(der_input)
p = pers.BatchPersonalization(
n=10,
src_pes=pes,
csv_rows=get_csv_reader())
p.add_param_and_src(
personalization.Iccid(),
param_source.IncDigitSource(
num_digits=18,
first_value=123456789012340001,
last_value=123456789012340010))
# add more parameters here, using ConfigurableParameter and ParamSource subclass instances to define the profile
# ...
# generate all 10 profiles (from n=10 above)
for result_pes in p.generate_profiles():
upp = result_pes.to_der()
store_upp(upp)
"""
class ParamAndSrc:
'tie a ConfigurableParameter to a source of actual values'
def __init__(self, param: ConfigurableParameter, src: param_source.ParamSource):
self.param = param
self.src = src
def __init__(self,
n: int,
src_pes: ProfileElementSequence,
params: list[ParamAndSrc]=None,
csv_rows: Generator=None,
):
"""
n: number of eSIM profiles to generate.
src_pes: a decoded eSIM profile as ProfileElementSequence, to serve as template. This is not modified, only
copied.
params: list of ParamAndSrc instances, defining a ConfigurableParameter and corresponding ParamSource to fill in
profile values.
csv_rows: A list or generator producing all CSV rows one at a time, starting with a row containing the column
headers. This is compatible with the python csv.reader. Each row gets passed to
ParamSource.get_next(), such that ParamSource implementations can access the row items.
See param_source.CsvSource.
"""
self.n = n
self.params = params or []
self.src_pes = src_pes
self.csv_rows = csv_rows
def add_param_and_src(self, param:ConfigurableParameter, src:param_source.ParamSource):
self.params.append(BatchPersonalization.ParamAndSrc(param=param, src=src))
def generate_profiles(self):
# get first row of CSV: column names
csv_columns = None
if self.csv_rows:
try:
csv_columns = next(self.csv_rows)
except StopIteration as e:
raise ValueError('the input CSV file appears to be empty') from e
for i in range(self.n):
csv_row = None
if self.csv_rows and csv_columns:
try:
csv_row_list = next(self.csv_rows)
except StopIteration as e:
raise ValueError(f'not enough rows in the input CSV for eSIM nr {i+1} of {self.n}') from e
csv_row = dict(zip(csv_columns, csv_row_list))
pes = copy.deepcopy(self.src_pes)
for p in self.params:
try:
input_value = p.src.get_next(csv_row=csv_row)
assert input_value is not None
value = p.param.__class__.validate_val(input_value)
p.param.__class__.apply_val(pes, value)
except Exception as e:
raise ValueError(f'{p.param.name} fed by {p.src.name}: {e}') from e
yield pes
class UppAudit(dict):
"""
Key-value pairs collected from a single UPP DER or PES.
UppAudit itself is a dict, callers may use the standard python dict API to access key-value pairs read from the UPP.
"""
@classmethod
def from_der(cls, der: bytes, params: List, der_size=False, additional_sd_keys=False):
'''return a dict of parameter name and set of selected parameter values found in a DER encoded profile. Note:
some ConfigurableParameter implementations return more than one key-value pair, for example, Imsi returns
both 'IMSI' and 'IMSI-ACC' parameters.
e.g.
UppAudit.from_der(my_der, [Imsi, ])
--> {'IMSI': '001010000000023', 'IMSI-ACC': '5'}
(where 'IMSI' == Imsi.name)
Read all parameters listed in params. params is a list of either ConfigurableParameter classes or
ConfigurableParameter class instances. This calls only classmethods, so each entry in params can either be the
class itself, or a class-instance of, a (non-abstract) ConfigurableParameter subclass.
For example, params = [Imsi, ] is equivalent to params = [Imsi(), ].
For der_size=True, also include a {'der_size':12345} entry.
For additional_sd_keys=True, output also all Security Domain KVN that there are *no* ConfigurableParameter
subclasses for. For example, SCP80 has reserved kvn 0x01..0x0f, but we offer only Scp80Kvn01, Scp80Kvn02,
Scp80Kvn03. So we would not show kvn 0x04..0x0f in an audit. additional_sd_keys=True includes audits of all SD
key KVN there may be in the UPP. This helps to spot SD keys that may already be present in a UPP template, with
unexpected / unusual kvn.
'''
# make an instance of this class
upp_audit = cls()
if der_size:
upp_audit['der_size'] = set((len(der), ))
pes = ProfileElementSequence.from_der(der)
for param in params:
try:
for valdict in param.get_values_from_pes(pes):
upp_audit.add_values(valdict)
except Exception as e:
raise ValueError(f'Error during audit for parameter {param}: {e}') from e
if not additional_sd_keys:
return upp_audit
# additional_sd_keys
for pe in pes.pe_list:
if pe.type != 'securityDomain':
continue
assert isinstance(pe, ProfileElementSD)
for key in pe.keys:
audit_key = f'SdKey_KVN{key.key_version_number:02x}_ID{key.key_identifier:02x}'
kuq_bin = KeyUsageQualifier.build(key.key_usage_qualifier).hex()
audit_val = f'{key.key_components=!r} key_usage_qualifier=0x{kuq_bin}={key.key_usage_qualifier!r}'
upp_audit[audit_key] = set((audit_val, ))
return upp_audit
def get_single_val(self, key, validate=True, allow_absent=False, absent_val=None):
"""
Return the audit's value for the given audit key (like 'IMSI' or 'IMSI-ACC').
Any kind of value may occur multiple times in a profile. When all of these agree to the same unambiguous value,
return that value. When they do not agree, raise a ValueError.
"""
# key should be a string, but if someone passes a ConfigurableParameter, just use its default name
if ConfigurableParameter.is_super_of(key):
key = key.get_name()
assert isinstance(key, str)
v = self.get(key)
if v is None and allow_absent:
return absent_val
if not isinstance(v, set):
raise ValueError(f'audit value should be a set(), got {v!r}')
if len(v) != 1:
raise ValueError(f'expected a single value for {key}, got {v!r}')
v = tuple(v)[0]
return v
@staticmethod
def audit_val_to_str(v):
"""
Usually, we want to see a single value in an audit. Still, to be able to collect multiple ambiguous values,
audit values are always python sets. Turn it into a nice string representation: only the value when it is
unambiguous, otherwise a list of the ambiguous values.
A value may also be completely absent, then return 'not present'.
"""
def try_single_val(w):
'change single-entry sets to just the single value'
if isinstance(w, set):
if len(w) == 1:
return tuple(w)[0]
if len(w) == 0:
return None
return w
v = try_single_val(v)
if isinstance(v, bytes):
v = bytes_to_hexstr(v)
if v is None:
return 'not present'
return str(v)
def get_val_str(self, key):
"""Return a string of the value stored for the given key"""
return UppAudit.audit_val_to_str(self.get(key))
def add_values(self, src:dict):
"""self and src are both a dict of sets.
For example from
self == { 'a': set((123,)) }
and
src == { 'a': set((456,)), 'b': set((789,)) }
then after this function call:
self == { 'a': set((123, 456,)), 'b': set((789,)) }
"""
assert isinstance(src, dict)
for key, srcvalset in src.items():
dstvalset = self.get(key)
if dstvalset is None:
dstvalset = set()
self[key] = dstvalset
dstvalset.add(srcvalset)
def __str__(self):
return '\n'.join(f'{key}: {self.get_val_str(key)}' for key in sorted(self.keys()))
class BatchAudit(list):
"""
Collect UppAudit instances for a batch of UPP, for example from a personalization.BatchPersonalization.
Produce an output CSV.
Usage example:
ba = BatchAudit(params=(personalization.Iccid, ))
for upp_der in upps:
ba.add_audit(upp_der)
print(ba.summarize())
with open('output.csv', 'wb') as csv_data:
csv_str = io.TextIOWrapper(csv_data, 'utf-8', newline='')
csv.writer(csv_str).writerows( ba.to_csv_rows() )
csv_str.flush()
BatchAudit itself is a list, callers may use the standard python list API to access the UppAudit instances.
"""
def __init__(self, params:List):
assert params
self.params = params
def add_audit(self, upp_der:bytes):
audit = UppAudit.from_der(upp_der, self.params)
self.append(audit)
return audit
def summarize(self):
batch_audit = UppAudit()
audits = self
if len(audits) > 2:
val_sep = ', ..., '
else:
val_sep = ', '
first_audit = None
last_audit = None
if len(audits) >= 1:
first_audit = audits[0]
if len(audits) >= 2:
last_audit = audits[-1]
if first_audit:
if last_audit:
for key in first_audit.keys():
first_val = first_audit.get_val_str(key)
last_val = last_audit.get_val_str(key)
if first_val == last_val:
val = first_val
else:
val_sep_with_newline = f"{val_sep.rstrip()}\n{' ' * (len(key) + 2)}"
val = val_sep_with_newline.join((first_val, last_val))
batch_audit[key] = val
else:
batch_audit.update(first_audit)
return batch_audit
def to_csv_rows(self, headers=True, sort_key=None):
'''generator that yields all audits' values as rows, useful feed to a csv.writer.'''
columns = set()
for audit in self:
columns.update(audit.keys())
columns = tuple(sorted(columns, key=sort_key))
if headers:
yield columns
for audit in self:
yield (audit.get_single_val(col, allow_absent=True, absent_val="") for col in columns)
def bytes_to_hexstr(b:bytes, sep=''):
return sep.join(f'{x:02x}' for x in b)
def esim_profile_introspect(upp):
pes = ProfileElementSequence.from_der(upp.read())
d = {}
d['upp'] = repr(pes)
def show_bytes_as_hexdump(item):
if isinstance(item, bytes):
return bytes_to_hexstr(item)
if isinstance(item, list):
return list(show_bytes_as_hexdump(i) for i in item)
if isinstance(item, tuple):
return tuple(show_bytes_as_hexdump(i) for i in item)
if isinstance(item, dict):
d = {}
for k, v in item.items():
d[k] = show_bytes_as_hexdump(v)
return d
return item
l = list((pe.type, show_bytes_as_hexdump(pe.decoded)) for pe in pes)
d['pp'] = pprint.pformat(l, width=120)
return d

View File

@@ -1,5 +1,5 @@
"""Implementation of SimAlliance/TCA Interoperable Profile OIDs"""
# 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
@@ -15,10 +15,8 @@
# 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]:
@@ -28,12 +26,8 @@ class OID:
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):
if type(initializer) == str:
self.intlist = self.intlist_from_str(initializer)
else:
self.intlist = initializer
@@ -44,49 +38,12 @@ class OID:
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):
if type(initializer) == str:
initializer = self.intlist_from_str(initializer)
super().__init__(self.__prefix + initializer)
@@ -96,18 +53,18 @@ 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")
ADF_USIM_not_by_default = eOID("2.5")
ADF_USIM_not_by_default_v2 = eOID("2.5.2")
ADF_USIM_not_by_default_v3 = eOID("2.5.3")
DF_PHONEBOOK_ADF_USIM = eOID("2.6")
DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
ADF_ISIM_by_default = eOID("2.8")
ADF_ISIMopt_not_by_default = eOID("2.9")
ADF_ISIMopt_not_by_default_v2 = eOID("2.9.2")
ADF_ISIM_not_by_default = eOID("2.9")
ADF_ISIM_not_by_default_v2 = eOID("2.9.2")
ADF_CSIM_by_default = eOID("2.10")
ADF_CSIM_by_default_v2 = eOID("2.10.2")
ADF_CSIMopt_not_by_default = eOID("2.11")
ADF_CSIMopt_not_by_default_v2 = eOID("2.11.2")
ADF_CSIM_not_by_default = eOID("2.11")
ADF_CSIM_not_by_default_v2 = eOID("2.11.2")
DF_EAP = eOID("2.12")
DF_5GS = eOID("2.13")
DF_5GS_v2 = eOID("2.13.2")
@@ -116,5 +73,5 @@ 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")
IoT_default = eOID("2.17")
IoT_default = eOID("2.18")

View File

@@ -1,228 +0,0 @@
# Implementation of SimAlliance/TCA Interoperable Profile handling: parameter sources for batch personalization.
#
# (C) 2025 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
#
# Author: nhofmeyr@sysmocom.de
#
# 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 secrets
import re
from osmocom.utils import b2h
class ParamSourceExn(Exception):
pass
class ParamSourceExhaustedExn(ParamSourceExn):
pass
class ParamSourceUndefinedExn(ParamSourceExn):
pass
class ParamSource:
"""abstract parameter source. For usage, see personalization.BatchPersonalization."""
# This name should be short but descriptive, useful for a user interface, like 'random decimal digits'.
name = "none"
numeric_base = None # or 10 or 16
@classmethod
def from_str(cls, s:str):
"""Subclasses implement this:
if a parameter source defines some string input magic, override this function.
For example, a RandomDigitSource derives the number of digits from the string length,
so the user can enter '0000' to get a four digit random number."""
return cls(s)
def get_next(self, csv_row:dict=None):
"""Subclasses implement this: return the next value from the parameter source.
When there are no more values from the source, raise a ParamSourceExhaustedExn.
This default implementation is an empty source."""
raise ParamSourceExhaustedExn()
class ConstantSource(ParamSource):
"""one value for all"""
name = "constant"
def __init__(self, val:str):
self.val = val
def get_next(self, csv_row:dict=None):
return self.val
class InputExpandingParamSource(ParamSource):
@classmethod
def expand_str(cls, s:str):
# user convenience syntax '0*32' becomes '00000000000000000000000000000000'
if "*" not in s:
return s
tokens = re.split(r"([^ \t]+)[ \t]*\*[ \t]*([0-9]+)", s)
if len(tokens) < 3:
return s
parts = []
for unchanged, snippet, repeat_str in zip(tokens[0::3], tokens[1::3], tokens[2::3]):
parts.append(unchanged)
repeat = int(repeat_str)
parts.append(snippet * repeat)
return "".join(parts)
@classmethod
def from_str(cls, s:str):
return cls(cls.expand_str(s))
class DecimalRangeSource(InputExpandingParamSource):
"""abstract: decimal numbers with a value range"""
numeric_base = 10
def __init__(self, num_digits, first_value, last_value):
"""
See also from_str().
All arguments are integer values, and are converted to int if necessary, so a string of an integer is fine.
num_digits: fixed number of digits (possibly with leading zeros) to generate.
first_value, last_value: the decimal range in which to provide digits.
"""
num_digits = int(num_digits)
first_value = int(first_value)
last_value = int(last_value)
assert num_digits > 0
assert first_value <= last_value
self.num_digits = num_digits
self.val_first_last = (first_value, last_value)
def val_to_digit(self, val:int):
return "%0*d" % (self.num_digits, val) # pylint: disable=consider-using-f-string
@classmethod
def from_str(cls, s:str):
s = cls.expand_str(s)
if ".." in s:
first_str, last_str = s.split('..')
first_str = first_str.strip()
last_str = last_str.strip()
else:
first_str = s.strip()
last_str = None
first_value = int(first_str)
last_value = int(last_str) if last_str is not None else "9" * len(first_str)
return cls(num_digits=len(first_str), first_value=first_value, last_value=last_value)
class RandomSourceMixin:
random_impl = secrets.SystemRandom()
class RandomDigitSource(DecimalRangeSource, RandomSourceMixin):
"""return a different sequence of random decimal digits each"""
name = "random decimal digits"
used_keys = set()
def get_next(self, csv_row:dict=None):
# try to generate random digits that are always different from previously produced random bytes
attempts = 10
while True:
val = self.random_impl.randint(*self.val_first_last)
if val in RandomDigitSource.used_keys:
attempts -= 1
if attempts:
continue
RandomDigitSource.used_keys.add(val)
break
return self.val_to_digit(val)
class RandomHexDigitSource(InputExpandingParamSource, RandomSourceMixin):
"""return a different sequence of random hexadecimal digits each"""
name = "random hexadecimal digits"
numeric_base = 16
used_keys = set()
def __init__(self, num_digits):
"""see from_str()"""
num_digits = int(num_digits)
if num_digits < 1:
raise ValueError("zero number of digits")
# hex digits always come in two
if (num_digits & 1) != 0:
raise ValueError(f"hexadecimal value should have even number of digits, not {num_digits}")
self.num_digits = num_digits
def get_next(self, csv_row:dict=None):
# try to generate random bytes that are always different from previously produced random bytes
attempts = 10
while True:
val = self.random_impl.randbytes(self.num_digits // 2)
if val in RandomHexDigitSource.used_keys:
attempts -= 1
if attempts:
continue
RandomHexDigitSource.used_keys.add(val)
break
return b2h(val)
@classmethod
def from_str(cls, s:str):
s = cls.expand_str(s)
return cls(num_digits=len(s.strip()))
class IncDigitSource(DecimalRangeSource):
"""incrementing sequence of digits"""
name = "incrementing decimal digits"
def __init__(self, num_digits, first_value, last_value):
super().__init__(num_digits, first_value, last_value)
self.next_val = None
self.reset()
def reset(self):
"""Restart from the first value of the defined range passed to __init__()."""
self.next_val = self.val_first_last[0]
def get_next(self, csv_row:dict=None):
val = self.next_val
if val is None:
raise ParamSourceExhaustedExn()
returnval = self.val_to_digit(val)
val += 1
if val > self.val_first_last[1]:
self.next_val = None
else:
self.next_val = val
return returnval
class CsvSource(ParamSource):
"""apply a column from a CSV row, as passed in to ParamSource.get_next(csv_row)"""
name = "from CSV"
def __init__(self, csv_column):
"""
csv_column: column name indicating the column to use for this parameter.
This name is used in get_next(): the caller passes the current CSV row to get_next(), from which
CsvSource picks the column with the name matching csv_column.
"""
self.csv_column = csv_column
def get_next(self, csv_row:dict=None):
val = None
if csv_row:
val = csv_row.get(self.csv_column)
if not val:
raise ParamSourceUndefinedExn(f"no value for CSV column {self.csv_column!r}")
return val

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
"""Implementation of SimAlliance/TCA Interoperable Profile Templates."""
# 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
@@ -17,36 +17,14 @@
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."""
"""Representation of a single file in a SimAlliance/TCA Profile Template."""
def __init__(self, fid:int, name:str, ftype, nb_rec: Optional[int], size:Optional[int], arr:int,
sfi:Optional[int] = None, default_val:Optional[str] = None, content_rqd:bool = True,
params:Optional[List] = None, ass_serv:Optional[List[int]]=None, high_update:bool = False,
pe_name:Optional[str] = None, 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.
"""
pe_name:Optional[str] = None):
# initialize from arguments
self.fid = fid
self.name = name
@@ -58,148 +36,46 @@ class FileTemplate:
if ftype in ['LF', 'CY']:
self.nb_rec = nb_rec
self.rec_len = size
elif ftype in ['TR', 'BT']:
elif ftype in ['TR']:
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
s_fid = "%04x" % self.fid if self.fid != None else 'None'
s_arr = self.arr if self.arr != None else 'None'
s_sfi = "%02x" % self.sfi if self.sfi != None else 'None'
return "FileTemplate(%s/%s, %s, %s, arr=%s, sfi=%s)" % (self.name, self.pe_name, s_fid,
self.file_type, s_arr, s_sfi)
class ProfileTemplate:
"""Representation of a SimAlliance/TCA Profile Template. Each Template is identified by its OID and
consists of a number of file definitions. We implement each profile template as a class derived from this
base class. Each such derived class is a singleton and has no instances."""
created_by_default: bool = False
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
files_by_pename: dict[str,FileTemplate] = {}
def __init_subclass__(cls, **kwargs):
"""This classmethod is called automatically after executing the subclass body. We use it to
initialize the cls.files_by_pename from the cls.files"""
super().__init_subclass__(**kwargs)
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."""
@@ -217,17 +93,17 @@ class ProfileTemplateRegistry:
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):
if type(oid) is not 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).
# of "Profile interoperability specification V3.1 Final" (unless other version explicitly specified).
# Section 9.2
class FilesAtMF(ProfileTemplate):
"""Files at MF as per Section 9.2"""
created_by_default = True
oid = OID.MF
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),
@@ -238,8 +114,8 @@ class FilesAtMF(ProfileTemplate):
]
# Section 9.3
class FilesCD(ProfileTemplate):
"""Files at DF.CD as per Section 9.3"""
created_by_default = False
oid = OID.DF_CD
files = [
@@ -253,47 +129,46 @@ class FilesCD(ProfileTemplate):
# 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]),
FileTemplate(0x4f30, 'EF.PBR', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size']),
]
for i in range(0x38, 0x40):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi']))
for i in range(0x40, 0x48):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
for i in range(0x48, 0x50):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size']))
df_pb_files += [
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi'], 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]),
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi']),
FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True),
]
for i in range(0x50, 0x58):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x58, 0x60):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x60, 0x68):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi']))
for i in range(0x68, 0x70):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x70, 0x78):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi']))
for i in range(0x78, 0x80):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x80, 0x88):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
for i in range(0x88, 0x90):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi']))
for i in range(0x90, 0x98):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi']))
for i in range(0x98, 0xa0):
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi']))
# Section 9.4 v2.3.1
class FilesTelecom(ProfileTemplate):
"""Files at DF.TELECOM as per Section 9.4 v2.3.1"""
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(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
@@ -301,40 +176,38 @@ class FilesTelecom(ProfileTemplate):
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]),
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size']),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
# EF.ICON below
]
for i in range(0x40, 0x80):
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size'], ppath=[0x5f50]))
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'], ppath=[0x5f50]))
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
df_pb = deepcopy(df_pb_files)
files += df_pb
files += [deepcopy(x) for x in df_pb_files]
files += [
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], 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(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
]
# Section 9.4
class FilesTelecomV2(ProfileTemplate):
"""Files at DF.TELECOM as per Section 9.4"""
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(0x7f11, 'DF.TELECOM', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
@@ -342,45 +215,45 @@ class FilesTelecomV2(ProfileTemplate):
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]),
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size']),
# EF.IIDF below
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size']),
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size']),
# EF.ICON below
]
for i in range(0x40, 0x80):
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size'], ppath=[0x5f50]))
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size']))
for i in range(0x80, 0xC0):
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'],ppath=[0x5f50]))
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size']))
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
df_pb = deepcopy(df_pb_files)
files += df_pb
files += [deepcopy(x) for x in df_pb_files]
files += [
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67]),
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], 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(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size']),
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True),
FileTemplate(0x5f3d, 'DF.MCS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}),
FileTemplate(0x5f3e, 'DF.V2X', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[119]),
FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119], 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
FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119]),
FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119]),
FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 2
FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119]), # VST: 3
]
# Section 9.5.1 v2.3.1
class FilesUsimMandatory(ProfileTemplate):
"""Mandatory Files at ADF.USIM as per Section 9.5.1 v2.3.1"""
created_by_default = True
oid = OID.ADF_USIM_by_default
files = [
@@ -410,8 +283,8 @@ class FilesUsimMandatory(ProfileTemplate):
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):
"""Mandatory Files at ADF.USIM as per Section 9.5.1"""
created_by_default = True
oid = OID.ADF_USIM_by_default_v2
files = [
@@ -442,13 +315,10 @@ class FilesUsimMandatoryV2(ProfileTemplate):
]
# Section 9.5.2 v2.3.1
class FilesUsimOptional(ProfileTemplate):
"""Optional Files at ADF.USIM as per Section 9.5.2 v2.3.1"""
created_by_default = False
optional = True
oid = OID.ADF_USIMopt_not_by_default
base_path = Path('ADF.USIM')
extends = FilesUsimMandatory
oid = OID.ADF_USIM_not_by_default
files = [
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13], pe_name='ef-acmax'),
@@ -463,9 +333,9 @@ class FilesUsimOptional(ProfileTemplate):
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(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]),
@@ -529,12 +399,8 @@ class FilesUsimOptional(ProfileTemplate):
# Section 9.5.2
class FilesUsimOptionalV2(ProfileTemplate):
"""Optional Files at ADF.USIM as per Section 9.5.2"""
created_by_default = False
optional = True
oid = OID.ADF_USIMopt_not_by_default_v2
base_path = Path('ADF.USIM')
extends = FilesUsimMandatoryV2
oid = OID.ADF_USIM_not_by_default_v2
files = [
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13]),
@@ -623,31 +489,18 @@ class FilesUsimOptionalV2(ProfileTemplate):
FileTemplate(0x6ffd, 'EF.MudMidCfgdata','BT', None, None,2, None, None, True, ['size'], ass_serv=[134]),
]
class FilesUsimOptionalV3(ProfileTemplate):
"""Optional Files at ADF.USIM as per Section 9.5.2.3 v3.3.1"""
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):
"""DF.PHONEBOOK Files at ADF.USIM as per Section 9.5.3"""
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):
"""DF.GSM-ACCESS Files at ADF.USIM as per Section 9.5.4"""
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),
@@ -657,12 +510,10 @@ class FilesUsimDfGsmAccess(ProfileTemplate):
]
# Section 9.5.11 v2.3.1
class FilesUsimDf5GS(ProfileTemplate):
"""DF.5GS Files at ADF.USIM as per Section 9.5.11 v2.3.1"""
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),
@@ -673,17 +524,15 @@ class FilesUsimDf5GS(ProfileTemplate):
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(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):
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.2"""
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),
@@ -701,12 +550,10 @@ class FilesUsimDf5GSv2(ProfileTemplate):
]
# Section 9.5.11.3
class FilesUsimDf5GSv3(ProfileTemplate):
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.3"""
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),
@@ -725,82 +572,23 @@ class FilesUsimDf5GSv3(ProfileTemplate):
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
]
class FilesUsimDf5GSv4(ProfileTemplate):
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.4"""
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):
"""DF.SAIP Files at ADF.USIM as per Section 9.5.12"""
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'),
FileTemplate(0x4f01, 'EF.SUCICalcInfo','TR', None, None, 3, None, 'FF..FF', False, ['size'], ass_serv=[125], pe_name='ef-suci-calc-info-usim'),
]
class FilesDfSnpn(ProfileTemplate):
"""DF.SNPN Files at ADF.USIM as per Section 9.5.13"""
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]),
]
class FilesDf5GProSe(ProfileTemplate):
"""DF.ProSe Files at ADF.USIM as per Section 9.5.14"""
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):
"""Mandatory Files at ADF.ISIM as per Section 9.6.1"""
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( 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']),
@@ -810,15 +598,12 @@ class FilesIsimMandatory(ProfileTemplate):
]
# Section 9.6.2 v2.3.1
class FilesIsimOptional(ProfileTemplate):
"""Optional Files at ADF.ISIM as per Section 9.6.2 of v2.3.1"""
created_by_default = False
optional = True
oid = OID.ADF_ISIMopt_not_by_default
base_path = Path('ADF.ISIM')
extends = FilesIsimMandatory
oid = OID.ADF_ISIM_not_by_default
files = [
FileTemplate(0x6f09, 'EF.P-CSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5], pe_name='ef-pcscf'),
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]),
@@ -830,13 +615,10 @@ class FilesIsimOptional(ProfileTemplate):
]
# Section 9.6.2
class FilesIsimOptionalv2(ProfileTemplate):
"""Optional Files at ADF.ISIM as per Section 9.6.2"""
created_by_default = False
optional = True
oid = OID.ADF_ISIMopt_not_by_default_v2
base_path = Path('ADF.ISIM')
extends = FilesIsimMandatory
oid = OID.ADF_ISIM_not_by_default_v2
files = [
FileTemplate(0x6f09, 'EF.PCSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
@@ -858,8 +640,8 @@ class FilesIsimOptionalv2(ProfileTemplate):
# TODO: CSIM
# Section 9.8
class FilesEap(ProfileTemplate):
"""Files at DF.EAP as per Section 9.8"""
created_by_default = False
oid = OID.DF_EAP
files = [
@@ -867,8 +649,8 @@ class FilesEap(ProfileTemplate):
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(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']),
]
@@ -891,85 +673,3 @@ ARR_DEFINITION = {
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,5 +1,5 @@
"""Implementation of SimAlliance/TCA Interoperable Profile validation."""
# 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
@@ -19,21 +19,16 @@
from pySim.esim.saip import *
class ProfileError(Exception):
"""Raised when a ProfileConstraintChecker finds an error in a file [structure]."""
pass
class ProfileConstraintChecker:
"""Base class of a constraint checker for a ProfileElementSequence."""
def check(self, pes: ProfileElementSequence):
"""Execute all the check_* methods of the ProfileConstraintChecker against the given
ProfileElementSequence"""
for name in dir(self):
if name.startswith('check_'):
method = getattr(self, name)
method(pes)
class CheckBasicStructure(ProfileConstraintChecker):
"""ProfileConstraintChecker for the basic profile structure constraints."""
def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
opt_pe = pes.get_pe_for_type(opt)
if opt_pe:
@@ -43,7 +38,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
# FIXME: check order
def check_start_and_end(self, pes: ProfileElementSequence):
"""Check for mandatory header and end ProfileElements at the right position."""
if pes.pe_list[0].type != 'header':
raise ProfileError('first element is not header')
if pes.pe_list[1].type != 'mf':
@@ -53,7 +47,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
raise ProfileError('last element is not end')
def check_number_of_occurrence(self, pes: ProfileElementSequence):
"""Check The number of occurrence of various ProfileElements."""
# check for invalid number of occurrences
if len(pes.get_pes_for_type('header')) != 1:
raise ProfileError('multiple ProfileHeader')
@@ -67,8 +60,7 @@ class CheckBasicStructure(ProfileConstraintChecker):
raise ProfileError('multiple PE-%s' % tn.upper())
def check_optional_ordering(self, pes: ProfileElementSequence):
"""Check the ordering of optional PEs following the respective mandatory ones."""
# ordering and required dependencies
# 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')
@@ -100,67 +92,5 @@ class CheckBasicStructure(ProfileConstraintChecker):
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):
if 'profile-a-p256' in m_svcs and not not ('usim' in m_svcs or 'isim' in m_svcs):
raise ProfileError('profile-a-p256 mandatory, but no usim or isim')
def check_mandatory_services_aka(self, pes: ProfileElementSequence):
"""Ensure that no unnecessary authentication related services are marked as mandatory but not
actually used within the profile"""
m_svcs = pes.get_pe_for_type('header').decoded['eUICC-Mandatory-services']
# list of tuples (algo_id, key_len_in_octets) for all the akaParameters in the PE Sequence
algo_id_klen = [(x.decoded['algoConfiguration'][1]['algorithmID'],
len(x.decoded['algoConfiguration'][1]['key'])) for x in pes.get_pes_for_type('akaParameter')]
# just a plain list of algorithm IDs in akaParameters
algorithm_ids = [x[0] for x in algo_id_klen]
if 'milenage' in m_svcs and not 1 in algorithm_ids:
raise ProfileError('milenage mandatory, but no related algorithm_id in akaParameter')
if 'tuak128' in m_svcs and not (2, 128/8) in algo_id_klen:
raise ProfileError('tuak128 mandatory, but no related algorithm_id in akaParameter')
if 'cave' in m_svcs and not pes.get_pe_for_type('cdmaParameter'):
raise ProfileError('cave mandatory, but no related cdmaParameter')
if 'tuak256' in m_svcs and (2, 256/8) in algo_id_klen:
raise ProfileError('tuak256 mandatory, but no related algorithm_id in akaParameter')
if 'usim-test-algorithm' in m_svcs and not 3 in algorithm_ids:
raise ProfileError('usim-test-algorithm mandatory, but no related algorithm_id in akaParameter')
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):
"""Raised when a FileConstraintChecker finds an error in a file [structure]."""
pass
class FileConstraintChecker:
def check(self, l: FileChoiceList):
"""Execute all the check_* methods of the FileConstraintChecker against the given FileChoiceList"""
for name in dir(self):
if name.startswith('check_'):
method = getattr(self, name)
method(l)
class FileCheckBasicStructure(FileConstraintChecker):
"""Validator for the basic structure of a decoded file."""
def check_seqence(self, l: FileChoiceList):
"""Check the sequence/ordering."""
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,5 +1,6 @@
"""Implementation of X.509 certificate handling in GSMA eSIM as per SGP22 v3.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
@@ -18,11 +19,11 @@
import requests
from typing import Optional, List
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import ec, padding
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
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
@@ -30,7 +31,7 @@ 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 cryptography 41.0.0 :(
# 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:
@@ -66,6 +67,7 @@ class oid:
class VerifyError(Exception):
"""An error during certificate verification,"""
pass
class CertificateSet:
"""A set of certificates consisting of a trusted [self-signed] CA root certificate,
@@ -86,7 +88,7 @@ class CertificateSet:
self.crl = None
def load_crl(self, urls: Optional[List[str]] = None):
if urls and isinstance(urls, str):
if urls and type(urls) is str:
urls = [urls]
if not urls:
# generate list of CRL URLs from root CA certificate
@@ -100,7 +102,7 @@ class CertificateSet:
for url in urls:
try:
crl_bytes = requests.get(url, timeout=10)
crl_bytes = requests.get(url)
except requests.exceptions.ConnectionError:
continue
crl = x509.load_der_x509_crl(crl_bytes)
@@ -149,7 +151,7 @@ class CertificateSet:
check_signed(c, self.root_cert)
return
parent_cert = self.intermediate_certs.get(aki, None)
if not parent_cert:
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)
@@ -158,6 +160,7 @@ class CertificateSet:
if depth > max_depth:
raise VerifyError('Maximum depth %u exceeded while verifying certificate chain' % max_depth)
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
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."""
@@ -188,7 +191,7 @@ class CertAndPrivkey:
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 "concatenated raw R + S values". """
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)

View File

@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
"""
Various definitions related to GSMA consumer + IoT eSIM / eUICC
Various definitions related to GSMA eSIM / eUICC
Does *not* implement anything related to M2M eUICC
Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
Related Specs: GSMA SGP.22, GSMA SGP.02, etc.
"""
# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
@@ -23,30 +21,20 @@ Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
# 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.tlv import *
from pySim.construct import *
from construct import Optional as COptional
from construct import *
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
from pySim.ts_102_221 import CardProfileUICC
from pySim.filesystem import CardADF, CardApplication
from pySim.utils import Hexstr, SwHexstr
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 type(eid) == str:
if len(eid) == 30:
# first pad by 2 digits
eid += "00"
@@ -56,7 +44,7 @@ def compute_eid_checksum(eid) -> str:
else:
raise ValueError("and EID must be 30 or 32 digits")
eid_int = int(eid)
elif isinstance(eid, int):
elif type(eid) == int:
eid_int = eid
if eid_int % 100:
# zero the last two digits
@@ -120,7 +108,7 @@ class SetDefaultDpAddress(BER_TLV_IE, tag=0xbf3f, nested=[DefaultDpAddress, SetD
# SGP.22 Section 5.7.7: GetEUICCChallenge
class EuiccChallenge(BER_TLV_IE, tag=0x80):
_construct = Bytes(16)
_construct = HexAdapter(Bytes(16))
class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
pass
@@ -128,7 +116,7 @@ class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
class SVN(BER_TLV_IE, tag=0x82):
_construct = VersionType
class SubjectKeyIdentifier(BER_TLV_IE, tag=0x04):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class EuiccCiPkiListForVerification(BER_TLV_IE, tag=0xa9, nested=[SubjectKeyIdentifier]):
pass
class EuiccCiPkiListForSigning(BER_TLV_IE, tag=0xaa, nested=[SubjectKeyIdentifier]):
@@ -140,15 +128,15 @@ class ProfileVersion(BER_TLV_IE, tag=0x81):
class EuiccFirmwareVer(BER_TLV_IE, tag=0x83):
_construct = VersionType
class ExtCardResource(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class UiccCapability(BER_TLV_IE, tag=0x85):
_construct = GreedyBytes # FIXME
_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 = GreedyBytes # FIXME
_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):
@@ -177,11 +165,11 @@ class ProfileMgmtOperation(BER_TLV_IE, tag=0x81):
class ListNotificationReq(BER_TLV_IE, tag=0xbf28, nested=[ProfileMgmtOperation]):
pass
class SeqNumber(BER_TLV_IE, tag=0x80):
_construct = Asn1DerInteger()
_construct = GreedyInteger()
class NotificationAddress(BER_TLV_IE, tag=0x0c):
_construct = Utf8Adapter(GreedyBytes)
class Iccid(BER_TLV_IE, tag=0x5a):
_construct = PaddedBcdAdapter(GreedyBytes)
_construct = BcdAdapter(GreedyBytes)
class NotificationMetadata(BER_TLV_IE, tag=0xbf2f, nested=[SeqNumber, ProfileMgmtOperation,
NotificationAddress, Iccid]):
pass
@@ -211,7 +199,7 @@ class TagList(BER_TLV_IE, tag=0x5c):
class ProfileInfoListReq(BER_TLV_IE, tag=0xbf2d, nested=[TagList]): # FIXME: SearchCriteria
pass
class IsdpAid(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class ProfileState(BER_TLV_IE, tag=0x9f70):
_construct = Enum(Int8ub, disabled=0, enabled=1)
class ProfileNickname(BER_TLV_IE, tag=0x90):
@@ -268,20 +256,9 @@ class DeleteProfileReq(BER_TLV_IE, tag=0xbf33, nested=[IsdpAid, Iccid]):
class DeleteProfileResp(BER_TLV_IE, tag=0xbf33, nested=[DeleteResult]):
pass
# SGP.22 Section 5.7.19: EuiccMemoryReset
class ResetOptions(BER_TLV_IE, tag=0x82):
_construct = FlagsEnum(Byte, deleteOperationalProfiles=0x80, deleteFieldLoadedTestProfiles=0x40,
resetDefaultSmdpAddress=0x20)
class ResetResult(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, ok=0, nothingToDelete=1, undefinedError=127)
class EuiccMemoryResetReq(BER_TLV_IE, tag=0xbf34, nested=[ResetOptions]):
pass
class EuiccMemoryResetResp(BER_TLV_IE, tag=0xbf34, nested=[ResetResult]):
pass
# SGP.22 Section 5.7.20 GetEID
class EidValue(BER_TLV_IE, tag=0x5a):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class GetEuiccData(BER_TLV_IE, tag=0xbf3e, nested=[TagList, EidValue]):
pass
@@ -302,7 +279,7 @@ 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):
class GetCertsError(BER_TLV_IE, tag=0x80):
_construct = Enum(Int8ub, invalidCiPKId=1, undefinedError=127)
class GetCertsResp(BER_TLV_IE, tag=0xbf56, nested=[EumCertificate, EuiccCertificate, GetCertsError]):
pass
@@ -315,9 +292,9 @@ class EimFqdn(BER_TLV_IE, tag=0x81):
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()
_construct = GreedyInteger
class AssociationToken(BER_TLV_IE, tag=0x84):
_construct = Asn1DerInteger()
_construct = GreedyInteger
class EimSupportedProtocol(BER_TLV_IE, tag=0x87):
_construct = Enum(Int8ub, eimRetrieveHttps=0, eimRetrieveCoaps=1, eimInjectHttps=2, eimInjectCoaps=3,
eimProprietary=4)
@@ -336,18 +313,16 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
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]:
def store_data(scc: SimCardCommands, tx_do: Hexstr) -> 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)
capdu = '%sE29100%02x%s' % (scc.cla4lchan('80'), len(tx_do)//2, tx_do)
return scc.send_apdu_checksw(capdu)
@staticmethod
def store_data_tlv(scc: SimCardCommands, cmd_do, resp_cls, exp_sw: SwMatchstr = '9000'):
def store_data_tlv(scc: SimCardCommands, cmd_do, resp_cls, exp_sw='9000'):
"""Transceive STORE DATA APDU with the card, transparently encoding the command data from TLV
and decoding the response data tlv."""
if cmd_do:
@@ -357,7 +332,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
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)
(data, sw) = CardApplicationISDR.store_data(scc, b2h(cmd_do_enc))
if data:
if resp_cls:
resp_do = resp_cls()
@@ -368,13 +343,6 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
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 b2h(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))
@@ -390,9 +358,9 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
@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)
(data, sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, opts.TX_DO)
def do_get_euicc_configured_addresses(self, _opts):
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()
@@ -409,25 +377,25 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
d = sdda.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['set_default_dp_address']))
def do_get_euicc_challenge(self, _opts):
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):
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):
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):
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()
@@ -444,7 +412,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
d = rn.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['notification_sent_resp']))
def do_get_profiles_info(self, _opts):
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()
@@ -461,11 +429,8 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
"""Perform an ES10c EnableProfile function."""
if opts.isdp_aid:
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
elif opts.iccid:
if 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)
@@ -483,11 +448,8 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
"""Perform an ES10c DisableProfile function."""
if opts.isdp_aid:
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
elif opts.iccid:
if 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)
@@ -504,52 +466,26 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
"""Perform an ES10c DeleteProfile function."""
if opts.isdp_aid:
p_id = IsdpAid(decoded=opts.isdp_aid)
elif opts.iccid:
if 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']))
mem_res_parser = argparse.ArgumentParser()
mem_res_parser.add_argument('--delete-operational', action='store_true',
help='Delete all operational profiles')
mem_res_parser.add_argument('--delete-test-field-installed', action='store_true',
help='Delete all test profiles, except pre-installed ones')
mem_res_parser.add_argument('--reset-smdp-address', action='store_true',
help='Reset the SM-DP+ address')
@cmd2.with_argparser(mem_res_parser)
def do_euicc_memory_reset(self, opts):
"""Perform an ES10c eUICCMemoryReset function. This will permanently delete the selected subset of
profiles from the eUICC."""
flags = {}
if opts.delete_operational:
flags['deleteOperationalProfiles'] = True
if opts.delete_test_field_installed:
flags['deleteFieldLoadedTestProfiles'] = True
if opts.reset_smdp_address:
flags['resetDefaultSmdpAddress'] = True
mr_cmd = EuiccMemoryResetReq(children=[ResetOptions(decoded=flags)])
mr = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, mr_cmd, EuiccMemoryResetResp)
d = mr.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['euicc_memory_reset_resp']))
def do_get_eid(self, _opts):
def do_get_eid(self, opts):
"""Perform an ES10c GetEID function."""
(data, sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, 'BF3E035C015A')
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')
set_nickname_parser.add_argument('--profile-nickname', help='Nickname of the profile')
@cmd2.with_argparser(set_nickname_parser)
def do_set_nickname(self, opts):
@@ -561,13 +497,13 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
d = sn.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['set_nickname_resp']))
def do_get_certs(self, _opts):
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']))
self._cmd.poutput_json(flatten_dict_lists(d['get_certficiates_resp']))
def do_get_eim_configuration_data(self, _opts):
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)
@@ -586,49 +522,7 @@ class CardApplicationECASD(pySim.global_platform.CardApplicationSD):
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
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

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

View File

@@ -17,97 +17,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import io
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 construct import *
from copy import deepcopy
from bidict import bidict
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.global_platform.install_param import gen_install_parameters
from pySim.construct import *
from pySim.utils import *
from pySim.filesystem import *
from pySim.tlv import *
from pySim.profile import CardProfile
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
from pySim.javacard import CapFile
# 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': {
@@ -203,16 +124,15 @@ 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)
KeyUsageQualifier = Struct('byte1'/FlagsEnum(Byte, verification_encryption=0x80,
computation_decipherment=0x40,
sm_response=0x20,
sm_command=0x10,
confidentiality=0x08,
crypto_checksum=0x04,
digital_signature=0x02,
crypto_authorization=0x01),
'byte2'/COptional(FlagsEnum(Byte, key_agreement=0x80)))
# 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,
@@ -290,13 +210,13 @@ class SupportedLFDBHAlgorithms(BER_TLV_IE, tag=0x83):
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)
CipherSuitesForSignatures = Struct('byte1'/FlagsEnum(Byte, rsa1024_pkcsv15_sha1=0x01,
rsa_gt1024_pss_sha256=0x02,
single_des_plus_final_triple_des_mac_16b=0x04,
cmac_aes128=0x08, cmac_aes192=0x10, cmac_aes256=0x20,
ecdsa_ecc256_sha256=0x40, ecdsa_ecc384_sha384=0x80),
'byte2'/COptional(FlagsEnum(Byte, ecdsa_ecc512_sha512=0x01,
ecdsa_ecc_521_sha512=0x02)))
class CiphersForTokens(BER_TLV_IE, tag=0x85):
_construct = CipherSuitesForSignatures
class CiphersForReceipts(BER_TLV_IE, tag=0x86):
@@ -318,7 +238,7 @@ class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
class ApplicationAID(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
pass
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
@@ -425,10 +345,10 @@ class FciTemplate(BER_TLV_IE, tag=0x6f, nested=FciTemplateNestedList):
pass
class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class CardImageNumber(BER_TLV_IE, tag=0x45):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
_construct = GreedyInteger()
@@ -467,16 +387,15 @@ class LifeCycleState(BER_TLV_IE, tag=0x9f70):
# Section 11.4.3.1 Table 11-36 + Section 11.1.2
class Privileges(BER_TLV_IE, tag=0xc5):
_construct = FlagsEnum(StripTrailerAdapter(GreedyBytes, 3, steps = [1, 3]),
security_domain=0x800000, dap_verification=0x400000,
delegated_management=0x200000, card_lock=0x100000, card_terminate=0x080000,
card_reset=0x040000, cvm_management=0x020000,
mandated_dap_verification=0x010000,
trusted_path=0x8000, authorized_management=0x4000,
token_management=0x2000, global_delete=0x1000, global_lock=0x0800,
global_registry=0x0400, final_application=0x0200, global_service=0x0100,
receipt_generation=0x80, ciphered_load_file_data_block=0x40,
contactless_activation=0x20, contactless_self_activation=0x10)
_construct = Struct('byte1'/FlagsEnum(Byte, security_domain=0x80, dap_verification=0x40,
delegated_management=0x20, card_lock=0x10, card_terminate=0x08,
card_reset=0x04, cvm_management=0x02,
mandated_dap_verification=0x01),
'byte2'/COptional(FlagsEnum(Byte, trusted_path=0x80, authorized_management=0x40,
token_management=0x20, global_delete=0x10, global_lock=0x08,
global_registry=0x04, final_application=0x02, global_service=0x01)),
'byte3'/COptional(FlagsEnum(Byte, 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):
@@ -487,7 +406,7 @@ class ImplicitSelectionParameter(BER_TLV_IE, tag=0xcf):
# Section 11.4.3.1 Table 11-36
class ExecutableLoadFileAID(BER_TLV_IE, tag=0xc4):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
@@ -495,15 +414,15 @@ class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
# 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 = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class ExecutableModuleAID(BER_TLV_IE, tag=0x84):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class AssociatedSecurityDomainAID(BER_TLV_IE, tag=0xcc):
_construct = GreedyBytes
_construct = HexAdapter(GreedyBytes)
# Section 11.4.3.1 Table 11-36
class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCycleState, Privileges,
@@ -524,11 +443,15 @@ class ADF_SD(CardADF):
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)
@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__()
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')
@@ -544,7 +467,7 @@ class ADF_SD(CardADF):
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)
(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())
@@ -570,19 +493,18 @@ class ADF_SD(CardADF):
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:]
chunk = remainder[:255]
remainder = remainder[255:]
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")
data, sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk))
block_nr += 1
response += data
return data
@@ -591,20 +513,15 @@ class ADF_SD(CardADF):
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-type', choices=KeyType.ksymapping.values(), action='append', required=True, help='Key Type')
put_key_parser.add_argument('--key-data', type=is_hexstr, action='append', required=True, help='Key Data Block')
put_key_parser.add_argument('--key-check', type=is_hexstr, action='append', help='Key Check Value')
put_key_parser.add_argument('--suppress-key-check', action='store_true', help='Suppress generation of Key Check Values')
@cmd2.with_argparser(put_key_parser)
def do_put_key(self, opts):
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
otherwise be automatically generated for DES and AES keys. You can suppress the latter using
`--suppress-key-check`.
Example (SCP80 KIC/KID/KIK):
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
--key-type aes --key-data 101112131415161718191a1b1c1d1e1f
@@ -621,18 +538,9 @@ class ADF_SD(CardADF):
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:
else:
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:
# encrypted 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})
kdb.append({'key_type': opts.key_type[i], 'kcb': opts.key_data[i], 'kcv': kcv})
p2 = opts.key_id
if len(opts.key_type) > 1:
p2 |= 0x80
@@ -640,19 +548,19 @@ class ADF_SD(CardADF):
# Table 11-68: Key Data Field - Format 1 (Basic Format)
KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
'kcb'/Prefixed(Int8ub, GreedyBytes),
'kcv'/Prefixed(Int8ub, GreedyBytes)))
'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")
data, sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data))
return data
get_status_parser = argparse.ArgumentParser()
get_status_parser.add_argument('subset', choices=list(StatusSubset.ksymapping.values()),
get_status_parser.add_argument('subset', choices=StatusSubset.ksymapping.values(),
help='Subset of statuses to be included in the response')
get_status_parser.add_argument('--aid', type=is_hexstr, default='',
help='AID Search Qualifier (search only for given AID)')
@@ -673,12 +581,12 @@ class ADF_SD(CardADF):
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")
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data))
remainder = h2b(data)
while len(remainder):
# tlv sequence, each element is one GpRegistryRelatedData()
grd = GpRegistryRelatedData()
_dec, remainder = grd.from_tlv(remainder)
dec, remainder = grd.from_tlv(remainder)
grd_list.append(grd)
if sw != '6310':
return grd_list
@@ -687,9 +595,9 @@ class ADF_SD(CardADF):
return grd_list
set_status_parser = argparse.ArgumentParser()
set_status_parser.add_argument('scope', choices=list(SetStatusScope.ksymapping.values()),
set_status_parser.add_argument('scope', choices=SetStatusScope.ksymapping.values(),
help='Defines the scope of the requested status change')
set_status_parser.add_argument('status', choices=list(CLifeCycleState.ksymapping.values()),
set_status_parser.add_argument('status', choices=CLifeCycleState.ksymapping.values(),
help='Specify the new intended status')
set_status_parser.add_argument('--aid', type=is_hexstr,
help='AID of the target Application or Security Domain')
@@ -704,82 +612,22 @@ class ADF_SD(CardADF):
def set_status(self, scope:str, status:str, aid:Hexstr = ''):
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
'scope'/SetStatusScope, 'status'/CLifeCycleState,
'aid'/Prefixed(Int8ub, COptional(GreedyBytes)))
'aid'/HexAdapter(Prefixed(Int8ub, Optional(GreedyBytes))))
apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
_data, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu))
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')
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
"""Perform GlobalPlatform INSTALL [for personalization] command in order toinform 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 (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
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'/Prefixed(Int8ub, GreedyBytes),
'module_aid'/Prefixed(Int8ub, GreedyBytes),
'application_aid'/Prefixed(Int8ub, GreedyBytes),
'privileges'/Prefixed(Int8ub, Privileges._construct),
'install_parameters'/Prefixed(Int8ub, GreedyBytes),
'install_token'/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))
inst_load_parser = argparse.ArgumentParser()
inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True,
help='AID of the loaded file')
inst_load_parser.add_argument('--security-domain-aid', type=is_hexstr, default='',
help='AID of the Security Domain into which the file shalle be added')
inst_load_parser.add_argument('--load-file-hash', type=is_hexstr, default='',
help='Load File Data Block Hash (GPC_SPE_034, section C.2)')
inst_load_parser.add_argument('--load-parameters', type=is_hexstr, default='',
help='Load Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
inst_load_parser.add_argument('--load-token', type=is_hexstr, default='',
help='Load Token (GPC_SPE_034, section C.4.1)')
@cmd2.with_argparser(inst_load_parser)
def do_install_for_load(self, opts):
"""Perform GlobalPlatform INSTALL [for load] command in order to prepare to load an application."""
if opts.load_token != '' and opts.load_file_hash == '':
raise ValueError('Load File Data Block Hash is mandatory if a Load Token is present')
InstallForLoadCD = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
'security_domain_aid'/Prefixed(Int8ub, GreedyBytes),
'load_file_hash'/Prefixed(Int8ub, GreedyBytes),
'load_parameters'/Prefixed(Int8ub, GreedyBytes),
'load_token'/Prefixed(Int8ub, GreedyBytes))
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
self.install(0x02, 0x00, b2h(ifl_bytes))
self.install(0x20, 0x00, "0000%02u%s000000" % (len(opts.application_aid)//2, opts.application_aid))
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
cmd_hex = "80E6%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
del_cc_parser = argparse.ArgumentParser()
@@ -807,138 +655,38 @@ class ADF_SD(CardADF):
"""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:
if opts.key_id == None and opts.key_ver == 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:
if opts.key_id != None:
cmd += "d001%02x" % opts.key_id
if opts.key_ver is not None:
if opts.key_ver != 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%s00" % (p1, p2, len(data)//2, data)
cmd_hex = "80E4%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
load_parser = argparse.ArgumentParser()
load_parser_from_grp = load_parser.add_mutually_exclusive_group(required=True)
load_parser_from_grp.add_argument('--from-hex', type=is_hexstr, help='load from hex string')
load_parser_from_grp.add_argument('--from-file', type=argparse.FileType('rb', 0), help='load from binary file')
load_parser_from_grp.add_argument('--from-cap-file', type=argparse.FileType('rb', 0), help='load from JAVA-card CAP file')
@cmd2.with_argparser(load_parser)
def do_load(self, opts):
"""Perform a GlobalPlatform LOAD command. (We currently only support loading without DAP and
without ciphering.)"""
if opts.from_hex is not None:
self.load(h2b(opts.from_hex))
elif opts.from_file is not None:
self.load(opts.from_file.read())
elif opts.from_cap_file is not None:
cap = CapFile(opts.from_cap_file)
self.load(cap.get_loadfile())
else:
raise ValueError('load source not specified!')
def load(self, contents:bytes, chunk_len:int = 240):
# TODO:tune chunk_len based on the overhead of the used SCP?
# build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case
remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents
# transfer this in various chunks to the card
total_size = len(remainder)
block_nr = 0
while len(remainder):
block = remainder[:chunk_len]
remainder = remainder[chunk_len:]
# build LOAD command APDU according to GPC_SPE_034 section 11.6.2 / Table 11-56
p1 = 0x00 if len(remainder) else 0x80
p2 = block_nr % 256
block_nr += 1
cmd_hex = "80E8%02x%02x%02x%s00" % (p1, p2, len(block), b2h(block))
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_install (and make selectable) now!" % (total_size, block_nr))
install_cap_parser = argparse.ArgumentParser()
install_cap_parser.add_argument('cap_file', type=str, metavar='FILE',
help='JAVA-CARD CAP file to install')
install_cap_parser_inst_prm_g = install_cap_parser.add_mutually_exclusive_group()
install_cap_parser_inst_prm_g.add_argument('--install-parameters', type=is_hexstr, default=None,
help='install Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_g_grp = install_cap_parser_inst_prm_g.add_argument_group()
install_cap_parser_inst_prm_g_grp.add_argument('--install-parameters-volatile-memory-quota',
type=int, default=None,
help='volatile memory quota (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_g_grp.add_argument('--install-parameters-non-volatile-memory-quota',
type=int, default=None,
help='non volatile memory quota (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
install_cap_parser_inst_prm_g_grp.add_argument('--install-parameters-stk',
type=is_hexstr, default=None,
help='Load Parameters (ETSI TS 102 226, section 8.2.1.3.2.1)')
@cmd2.with_argparser(install_cap_parser)
def do_install_cap(self, opts):
"""Perform a .cap file installation using GlobalPlatform LOAD and INSTALL commands."""
self._cmd.poutput("loading cap file: %s ..." % opts.cap_file)
cap = CapFile(opts.cap_file)
security_domain_aid = self._cmd.lchan.selected_file.aid
load_file = cap.get_loadfile()
load_file_aid = cap.get_loadfile_aid()
module_aid = cap.get_applet_aid()
application_aid = module_aid
if opts.install_parameters:
install_parameters = opts.install_parameters;
else:
install_parameters = gen_install_parameters(opts.install_parameters_non_volatile_memory_quota,
opts.install_parameters_volatile_memory_quota,
opts.install_parameters_stk)
self._cmd.poutput("parameters:")
self._cmd.poutput(" security-domain-aid: %s" % security_domain_aid)
self._cmd.poutput(" load-file: %u bytes" % len(load_file))
self._cmd.poutput(" load-file-aid: %s" % load_file_aid)
self._cmd.poutput(" module-aid: %s" % module_aid)
self._cmd.poutput(" application-aid: %s" % application_aid)
self._cmd.poutput(" install-parameters: %s" % install_parameters)
self._cmd.poutput("step #1: install for load...")
self.do_install_for_load("--load-file-aid %s --security-domain-aid %s" % (load_file_aid, security_domain_aid))
self._cmd.poutput("step #2: load...")
self.load(load_file)
self._cmd.poutput("step #3: install_for_install (and make selectable)...")
self.do_install_for_install("--load-file-aid %s --module-aid %s --application-aid %s --install-parameters %s --make-selectable" %
(load_file_aid, module_aid, application_aid, install_parameters))
self._cmd.poutput("done.")
est_scp02_parser = argparse.ArgumentParser()
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, default=0, help='Key Version Number (KVN)')
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, required=True,
help='Key Version Number (KVN)')
est_scp02_parser.add_argument('--key-enc', type=is_hexstr, required=True,
help='Secure Channel Encryption Key')
est_scp02_parser.add_argument('--key-mac', type=is_hexstr, required=True,
help='Secure Channel MAC Key')
est_scp02_parser.add_argument('--key-dek', type=is_hexstr, required=True,
help='Data Encryption Key')
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
@@ -948,24 +696,12 @@ class ADF_SD(CardADF):
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
@@ -978,17 +714,17 @@ class ADF_SD(CardADF):
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))
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))
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):
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")
@@ -1002,9 +738,6 @@ 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):
@@ -1012,14 +745,18 @@ class CardApplicationISD(CardApplicationSD):
# 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):
# The Key Version Number is an 8 bit integer number, where 0 refers to the first available key,
# see also: GPC_SPE_034, section E.5.1.3
assert 0 <= kvn < 256
assert kvn >= 0 and kvn < 256
assert len(enc) == len(mac) == len(dek)
self.kvn = kvn
self.enc = enc
@@ -1028,39 +765,8 @@ class GpCardKeyset:
@classmethod
def from_single_key(cls, kvn: int, base_key: bytes) -> 'GpCardKeyset':
return cls(kvn, base_key, base_key, base_key)
return cls(int, 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,94 +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, GreedyString, BytesInteger
from construct import this, len_, Rebuild, Const
from construct import Optional as COptional
from osmocom.construct import Bytes, GreedyBytes
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,72 +0,0 @@
# GlobalPlatform install parameter generator
#
# (C) 2024 by sysmocom - s.f.m.c. GmbH
# 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 osmocom.construct import *
from osmocom.utils import *
from osmocom.tlv import *
class AppSpecificParams(BER_TLV_IE, tag=0xC9):
# GPD_SPE_013, table 11-49
_construct = GreedyBytes
class VolatileMemoryQuota(BER_TLV_IE, tag=0xC7):
# GPD_SPE_013, table 11-49
_construct = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
class NonVolatileMemoryQuota(BER_TLV_IE, tag=0xC8):
# GPD_SPE_013, table 11-49
_construct = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
class StkParameter(BER_TLV_IE, tag=0xCA):
# GPD_SPE_013, table 11-49
# ETSI TS 102 226, section 8.2.1.3.2.1
_construct = GreedyBytes
class SystemSpecificParams(BER_TLV_IE, tag=0xEF, nested=[VolatileMemoryQuota, NonVolatileMemoryQuota, StkParameter]):
# GPD_SPE_013 v1.1 Table 6-5
pass
class InstallParams(TLV_IE_Collection, nested=[AppSpecificParams, SystemSpecificParams]):
# GPD_SPE_013, table 11-49
pass
def gen_install_parameters(non_volatile_memory_quota:int, volatile_memory_quota:int, stk_parameter:str):
# GPD_SPE_013, table 11-49
#Mandatory
install_params = InstallParams()
install_params_dict = [{'app_specific_params': None}]
#Conditional
if non_volatile_memory_quota and volatile_memory_quota and stk_parameter:
system_specific_params = []
#Optional
if non_volatile_memory_quota:
system_specific_params += [{'non_volatile_memory_quota': non_volatile_memory_quota}]
#Optional
if volatile_memory_quota:
system_specific_params += [{'volatile_memory_quota': volatile_memory_quota}]
#Optional
if stk_parameter:
system_specific_params += [{'stk_parameter': stk_parameter}]
install_params_dict += [{'system_specific_params': system_specific_params}]
install_params.from_dict(install_params_dict)
return b2h(install_params.to_bytes())

View File

@@ -20,21 +20,18 @@ import logging
from typing import Optional
from Cryptodome.Cipher import DES3, DES
from Cryptodome.Util.strxor import strxor
from construct import Struct, Int8ub, Int16ub, Const
from construct import Struct, Bytes, Int8ub, Int16ub, Const
from construct import Optional as COptional
from osmocom.construct import Bytes
from osmocom.utils import b2h
from osmocom.tlv import bertlv_parse_len, bertlv_encode_len
from pySim.utils import parse_command_apdu
from pySim.utils import b2h
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(len(constant) == 2)
assert(counter >= 0 and counter <= 65535)
assert len(base_key) == 16
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)
@@ -61,7 +58,6 @@ class Scp02SessionKeys:
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"""
@@ -114,48 +110,10 @@ 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):
# Spec references that explain KVN ranges:
# TS 102 225 Annex A.1 states KVN 0x01..0x0F shall be used for SCP80
# GPC_GUI_003 states
# * For the Issuer Security Domain, this is initially Key Version Number 'FF' which has been deliberately
# chosen to be outside of the allowable range ('01' to '7F') for a Key Version Number.
# * It is logical that the initial keys in the Issuer Security Domain be replaced by an initial issuer Key
# Version Number in the range '01' to '6F'.
# * Key Version Numbers '70' to '72' and '74' to '7F' are reserved for future use.
# * On an implementation supporting Supplementary Security Domains, the RSA public key with a Key Version
# Number '73' and a Key Identifier of '01' has the following functionality in a Supplementary Security
# Domain with the DAP Verification privilege [...]
# GPC_GUI_010 V1.0.1 Section 6 states
# * Key Version number range ('20' to '2F') is reserved for SCP02
# * Key Version 'FF' is reserved for use by an Issuer Security Domain supporting SCP02, and cannot be used
# for SCP80. This initial key set shall be replaced by a key set with a Key Version Number in the
# ('20' to '2F') range.
# * Key Version number range ('01' to '0F') is reserved for SCP80
# * Key Version number '70' with Key Identifier '01' is reserved for the Token Key, which is either a RSA
# public key or a DES key
# * Key Version number '71' with Key Identifier '01' is reserved for the Receipt Key, which is a DES key
# * Key Version Number '11' is reserved for DAP as specified in ETSI TS 102 226 [2]
# * Key Version Number '73' with Key Identifier '01' is reserved for the DAP verification key as specified
# in sections 3.3.3 and 4 of [4], which is either an RSA public key or DES key
# * Key Version Number '74' is reserved for the CASD Keys (cf. section 9.2)
# * Key Version Number '75' with Key Identifier '01' is reserved for the key used to decipher the Ciphered
# Load File Data Block described in section 4.8 of [5].
if card_keys.kvn == 0:
# Key Version Number 0x00 refers to the first available key, so we won't carry out
# a range check in this case. See also: GPC_SPE_034, section E.5.1.3
pass
elif hasattr(self, 'kvn_range'):
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
@@ -196,7 +154,8 @@ class SCP(SecureChannel, abc.ABC):
# only protect those APDUs that actually are global platform commands
if apdu[0] & 0x80:
return self._wrap_cmd_apdu(apdu, *args, **kwargs)
return apdu
else:
return apdu
@abc.abstractmethod
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
@@ -215,63 +174,13 @@ class SCP(SecureChannel, abc.ABC):
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))
# Key Version Number 0x70 is a non-spec special-case of sysmoISIM-SJA2/SJA5 and possibly more sysmocom products
# Key Version Number 0x01 is a non-spec special-case of sysmoUSIM-SJS1
kvn_ranges = [[0x01, 0x01], [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)
kvn_range = [0x20, 0x2f]
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge))
@@ -282,7 +191,7 @@ class SCP02(SCP):
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'
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge
def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALZIE UPDATE."""
@@ -307,59 +216,41 @@ class SCP02(SCP):
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:
def _wrap_cmd_apdu(self, apdu: bytes) -> bytes:
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
lc = len(apdu) - 5
assert len(apdu) >= 5, "Wrong APDU length: %d" % len(apdu)
assert len(apdu) == 5 or apdu[4] == lc, "Lc differs from length of data: %d vs %d" % (apdu[4], lc)
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
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'
# CLA without log. channel can be 80 or 00 only
if self.do_cmac:
if self.mac_on_unmodified:
mlc = lc
clac = cla
else: # CMAC on modified APDU
mlc = lc + 8
clac = cla | CLA_SM
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + apdu[5:])
if self.do_cenc:
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
data = k.encrypt(pad80(apdu[5:], 8))
lc = len(data)
else:
data = apdu[5:]
lc += 8
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
return apdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
def unwrap_rsp_apdu(self, sw: bytes, apdu: bytes) -> bytes:
# TODO: Implement R-MAC / R-ENC
return rsp_apdu
return apdu
@@ -373,7 +264,7 @@ def scp03_key_derivation(constant: bytes, context: bytes, base_key: bytes, l: Op
def prf(key: bytes, data:bytes):
return CMAC.new(key, data, AES).digest()
if l is None:
if l == 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)
@@ -475,17 +366,8 @@ class SCP03(SCP):
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
@@ -496,12 +378,12 @@ class SCP03(SCP):
def gen_init_update_apdu(self, host_challenge: Optional[bytes] = None) -> bytes:
"""Generate INITIALIZE UPDATE APDU."""
if host_challenge is None:
if host_challenge == 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'
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge
def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALIZE UPDATE."""
@@ -526,26 +408,18 @@ class SCP03(SCP):
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
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
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
lc = apdu[4]
assert lc == len(apdu) - 5
cmd_data = apdu[5:]
if self.do_cenc and not skip_cenc:
if case <= 2:
assert self.do_cmac
if lc == 0:
# No encryption shall be applied to a command where there is no command data field. In this
# case, the encryption counter shall still be incremented
self.sk.block_nr += 1
@@ -558,39 +432,36 @@ class SCP03(SCP):
# 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]
if self.do_cmac:
# The length of the command message (Lc) shall be incremented by 8 (in S8 mode) or 16 (in S16
# mode) to indicate the inclusion of the C-MAC in the data field of the command message.
mlc = lc + self.s_mode
if mlc >= 256:
raise ValueError('Modified Lc (%u) would exceed maximum when appending %u bytes of mac' % (mlc, self.s_mode))
# The class byte shall be modified for the generation or verification of the C-MAC: The logical
# channel number shall be set to zero, bit 4 shall be set to 0 and bit 3 shall be set to 1 to indicate
# GlobalPlatform proprietary secure messaging.
mcla = (cla & 0xF0) | CLA_SM
mapdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
cmac = self.sk.calc_cmac(mapdu)
mapdu += cmac[:self.s_mode]
# See comment in SCP03._wrap_cmd_apdu()
if case == 4 or case == 2:
apdu += b'\x00'
return mapdu
return apdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
def unwrap_rsp_apdu(self, sw: bytes, 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)
logger.debug("unwrap_rsp_apdu(sw=%s, apdu=%s)", sw, apdu)
if not self.do_rmac:
assert not self.do_renc
return rsp_apdu
return 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:]
return apdu
response_data = apdu[:-self.s_mode]
rmac = 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))

View File

@@ -1,108 +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 0x81 .. 0x8f reserved for SCP81
# 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 support

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, 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_51_011
######################################################################
# DF.EIRENE (FFFIS for GSM-R SIM Cards)
@@ -71,15 +77,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 +94,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 +111,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 +131,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
@@ -184,13 +190,13 @@ class EF_CallconfI(LinFixedEF):
class EF_Shunting(TransparentEF):
"""Section 7.6"""
_test_de_encode = [
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": h2b("f8ffffff000000") } ),
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": "f8ffffff000000" } ),
]
def __init__(self):
super().__init__(fid='6ff4', sfid=None,
name='EF.Shunting', desc='Shunting', size=(8, 8))
self._construct = Struct('common_gid'/Int8ub,
'shunting_gid'/Bytes(7))
'shunting_gid'/HexAdapter(Bytes(7)))
class EF_GsmrPLMN(LinFixedEF):
@@ -199,13 +205,13 @@ class EF_GsmrPLMN(LinFixedEF):
( "22f860f86f8d6f8e01", { "plmn": "228-06", "class_of_network": {
"supported": { "vbs": True, "vgcs": True, "emlpp": True,
"fn": True, "eirene": True }, "preference": 0 },
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
"ic_table_ref": h2b("01") } ),
"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": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
"ic_table_ref": h2b("02") } ),
"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',
@@ -213,24 +219,24 @@ class EF_GsmrPLMN(LinFixedEF):
self._construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
'class_of_network'/BitStruct('supported'/FlagsEnum(BitsInteger(5), vbs=1, vgcs=2, emlpp=4, fn=8, eirene=16),
'preference'/BitsInteger(3)),
'ic_incoming_ref_tbl'/Bytes(2),
'outgoing_ref_tbl'/Bytes(2),
'ic_table_ref'/Bytes(1))
'ic_incoming_ref_tbl'/HexAdapter(Bytes(2)),
'outgoing_ref_tbl'/HexAdapter(Bytes(2)),
'ic_table_ref'/HexAdapter(Bytes(1)))
class EF_IC(LinFixedEF):
"""Section 7.8"""
_test_de_encode = [
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": h2b("6f8e"),
( "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": h2b("ffff"),
( "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'/Bytes(2),
'id_of_next_table'/HexAdapter(Bytes(2)),
'ic_decision_value'/BcdAdapter(Bytes(2)),
'network_string_table_index'/Int16ub)
@@ -252,18 +258,18 @@ 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": h2b("6f87"),
( "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": h2b("6f8f"),
( "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": h2b("6f89"),
( "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):
super().__init__(fid=fid, sfid=None,
name=name, desc=desc, rec_len=(6, 6))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/Bytes(2),
'id_of_next_table'/HexAdapter(Bytes(2)),
'decision_value'/BcdAdapter(Bytes(2)),
'string_table_index'/Int8ub)
@@ -271,12 +277,12 @@ 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": h2b("6f85") } ),
( "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'/Bytes(2))
'id_of_next_table'/HexAdapter(Bytes(2)))
construct_others = Struct('predefined_value1'/BcdAdapter(Bytes(2)),
'string_table_index1'/Int8ub)
@@ -290,7 +296,7 @@ class EF_Predefined(LinFixedEF):
else:
return parse_construct(self.construct_others, raw_bin_data)
def _encode_record_bin(self, abstract_data : dict, record_nr : int, **kwargs) -> bytearray:
def _encode_record_bin(self, abstract_data : dict, record_nr : int) -> bytearray:
r = None
if record_nr == 1:
r = self.construct_first.build(abstract_data)
@@ -301,13 +307,13 @@ class EF_Predefined(LinFixedEF):
class EF_DialledVals(TransparentEF):
"""Section 8.6"""
_test_de_encode = [
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": h2b("ffff"), "dialed_digits": "22" } ),
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": h2b("6f88"), "dialed_digits": "58" }),
( "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):
super().__init__(fid=fid, sfid=None, name=name, desc=desc, size=(4, 4))
self._construct = Struct('next_table_type'/NextTableType,
'id_of_next_table'/Bytes(2),
'id_of_next_table'/HexAdapter(Bytes(2)),
'dialed_digits'/BcdAdapter(Bytes(1)))

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 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,142 +0,0 @@
# JavaCard related utilities
#
# (C) 2024 by sysmocom - s.f.m.c. GmbH
# 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/>.
#
import zipfile
import struct
import sys
import io
from osmocom.utils import b2h, Hexstr
from construct import Struct, Array, this, Int32ub, Int16ub, Int8ub
from osmocom.construct import *
from osmocom.tlv import *
from construct import Optional as COptional
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.
example usage:
with io.open(sys.argv[1],"rb") as f, zipfile.ZipFile(sys.argv[2], "wb") as z:
ijc_to_cap(f, z)
"""
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:]
class CapFile():
# Java Card Platform Virtual Machine Specification, v3.2, section 6.4
__header_component_compact = Struct('tag'/Int8ub,
'size'/Int16ub,
'magic'/Int32ub,
'minor_version'/Int8ub,
'major_version'/Int8ub,
'flags'/Int8ub,
'package'/Struct('minor_version'/Int8ub,
'major_version'/Int8ub,
'AID'/LV),
'package_name'/COptional(LV)) #since CAP format 2.2
# Java Card Platform Virtual Machine Specification, v3.2, section 6.6
__applet_component_compact = Struct('tag'/Int8ub,
'size'/Int16ub,
'count'/Int8ub,
'applets'/Array(this.count, Struct('AID'/LV,
'install_method_offset'/Int16ub)),
)
def __init__(self, filename:str):
# In this dictionary we will keep all nested .cap file components by their file names (without .cap suffix)
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2.1
self.__component = {}
# Extract the nested .cap components from the .cap file
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2.1
cap = zipfile.ZipFile(filename)
cap_namelist = cap.namelist()
for i, filename in enumerate(cap_namelist):
if filename.lower().endswith('.capx') and not filename.lower().endswith('.capx'):
#TODO: At the moment we only support the compact .cap format, add support for the extended .cap format.
raise ValueError("incompatible .cap file, extended .cap format not supported!")
if filename.lower().endswith('.cap'):
key = filename.split('/')[-1].removesuffix('.cap')
self.__component[key] = cap.read(filename)
# Make sure that all mandatory components are present
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2
required_components = {'Header' : 'COMPONENT_Header',
'Directory' : 'COMPONENT_Directory',
'Import' : 'COMPONENT_Import',
'ConstantPool' : 'COMPONENT_ConstantPool',
'Class' : 'COMPONENT_Class',
'Method' : 'COMPONENT_Method',
'StaticField' : 'COMPONENT_StaticField',
'RefLocation' : 'COMPONENT_ReferenceLocation',
'Descriptor' : 'COMPONENT_Descriptor'}
for component in required_components:
if component not in self.__component.keys():
raise ValueError("invalid cap file, %s missing!" % required_components[component])
def get_loadfile(self) -> bytes:
"""Get the executable loadfile as hexstring"""
# Concatenate all cap file components in the specified order
# see also: Java Card Platform Virtual Machine Specification, v3.2, section 6.3
loadfile = self.__component['Header']
loadfile += self.__component['Directory']
loadfile += self.__component['Import']
if 'Applet' in self.__component.keys():
loadfile += self.__component['Applet']
loadfile += self.__component['Class']
loadfile += self.__component['Method']
loadfile += self.__component['StaticField']
if 'Export' in self.__component.keys():
loadfile += self.__component['Export']
loadfile += self.__component['ConstantPool']
loadfile += self.__component['RefLocation']
if 'Descriptor' in self.__component.keys():
loadfile += self.__component['Descriptor']
return loadfile
def get_loadfile_aid(self) -> Hexstr:
"""Get the loadfile AID as hexstring"""
header = self.__header_component_compact.parse(self.__component['Header'])
magic = header['magic'] or 0
if magic != 0xDECAFFED:
raise ValueError("invalid cap file, COMPONENT_Header lacks magic number (0x%08X!=0xDECAFFED)!" % magic)
#TODO: check cap version and make sure we are compatible with it
return header['package']['AID']
def get_applet_aid(self, index:int = 0) -> Hexstr:
"""Get the applet AID as hexstring"""
#To get the module AID, we must look into COMPONENT_Applet. Unfortunately, even though this component should
#be present in any .cap file, it is defined as an optional component.
if 'Applet' not in self.__component.keys():
raise ValueError("can't get the AID, this cap file lacks the optional COMPONENT_Applet component!")
applet = self.__applet_component_compact.parse(self.__component['Applet'])
if index > applet['count']:
raise ValueError("can't get the AID for applet with index=%u, this .cap file only has %u applets!" %
(index, applet['count']))
return applet['applets'][index]['AID']

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

View File

@@ -3,14 +3,15 @@
################################################################################
import abc
from smartcard.util import toBytes
from pytlv.TLV import *
from pySim.cards import SimCardBase, UiccCardBase
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi, dec_msisdn, enc_msisdn
from pySim.utils import enc_plmn, get_addr_type
from pySim.utils import is_hex, h2b, b2h, h2s, s2h, lpad, rpad
from pySim.legacy.utils import enc_ePDGSelection, format_xplmn_w_act, format_xplmn, dec_st, enc_st
from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv, dec_msisdn, enc_msisdn
from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv
from pySim.legacy.ts_51_011 import EF, DF
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
from pySim.legacy.ts_31_103 import EF_ISIM_ADF_map
@@ -495,7 +496,7 @@ class IsimCard(UiccCardBase):
class MagicSimBase(abc.ABC, SimCard):
"""
These cards uses several record based EFs to store the provider infos,
Theses cards uses several record based EFs to store the provider infos,
each possible provider uses a specific record number in each EF. The
indexes used are ( where N is the number of providers supported ) :
- [2 .. N+1] for the operator name
@@ -644,7 +645,7 @@ class MagicSim(MagicSimBase):
class FakeMagicSim(SimCard):
"""
These cards have a record based EF 3f00/000c that contains the provider
Theses cards have a record based EF 3f00/000c that contains the provider
information. See the program method for its format. The records go from
1 to N.
"""
@@ -780,7 +781,7 @@ class SysmoSIMgr1(GrcardSim):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b991800118822334455667760":
if scc.get_atr() == toBytes("3B 99 18 00 11 88 22 33 44 55 66 77 60"):
return kls(scc)
except:
return None
@@ -825,7 +826,7 @@ class SysmoSIMgr2(SimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b7d9400005555530a7486930b247c4d5468":
if scc.get_atr() == toBytes("3B 7D 94 00 00 55 55 53 0A 74 86 93 0B 24 7C 4D 54 68"):
return kls(scc)
except:
return None
@@ -903,7 +904,7 @@ class SysmoUSIMSJS1(UsimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b9f96801fc78031a073be21136743200718000001a5":
if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 43 20 07 18 00 00 01 A5"):
return kls(scc)
except:
return None
@@ -1031,7 +1032,7 @@ class FairwavesSIM(UsimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b9f96801fc78031a073be21136744220610000001a9":
if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 44 22 06 10 00 00 01 A9"):
return kls(scc)
except:
return None
@@ -1165,7 +1166,7 @@ class OpenCellsSim(SimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b9f95801fc38031e073fe21135786810286984418a8":
if scc.get_atr() == toBytes("3B 9F 95 80 1F C3 80 31 E0 73 FE 21 13 57 86 81 02 86 98 44 18 A8"):
return kls(scc)
except:
return None
@@ -1214,7 +1215,7 @@ class WavemobileSim(UsimCard):
def autodetect(kls, scc):
try:
# Look for ATR
if scc.get_atr() == "3b9f95801fc78031e073f62113674d4516004301008f":
if scc.get_atr() == toBytes("3B 9F 95 80 1F C7 80 31 E0 73 F6 21 13 67 4D 45 16 00 43 01 00 8F"):
return kls(scc)
except:
return None
@@ -1304,18 +1305,18 @@ class SysmoISIMSJA2(UsimCard, IsimCard):
def autodetect(kls, scc):
try:
# Try card model #1
atr = "3b9f96801f878031e073fe211b674a4c753034054ba9"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9"
if scc.get_atr() == toBytes(atr):
return kls(scc)
# Try card model #2
atr = "3b9f96801f878031e073fe211b674a4c7531330251b2"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2"
if scc.get_atr() == toBytes(atr):
return kls(scc)
# Try card model #3
atr = "3b9f96801f878031e073fe211b674a4c5275310451d5"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"
if scc.get_atr() == toBytes(atr):
return kls(scc)
except:
return None
@@ -1553,16 +1554,16 @@ class SysmoISIMSJA5(SysmoISIMSJA2):
def autodetect(kls, scc):
try:
# Try card model #1 (9FJ)
atr = "3b9f96801f878031e073fe211b674a357530350251cc"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC"
if scc.get_atr() == toBytes(atr):
return kls(scc)
# Try card model #2 (SLM17)
atr = "3b9f96801f878031e073fe211b674a357530350265f8"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8"
if scc.get_atr() == toBytes(atr):
return kls(scc)
# Try card model #3 (9FV)
atr = "3b9f96801f878031e073fe211b674a357530350259c4"
if scc.get_atr() == atr:
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"
if scc.get_atr() == toBytes(atr):
return kls(scc)
except:
return None
@@ -1591,7 +1592,7 @@ class GialerSim(UsimCard):
def autodetect(cls, scc):
try:
# Look for ATR
if scc.get_atr() == '3b9f95801fc78031a073b6a10067cf3215ca9cd70920':
if scc.get_atr() == toBytes('3B 9F 95 80 1F C7 80 31 A0 73 B6 A1 00 67 CF 32 15 CA 9C D7 09 20'):
return cls(scc)
except:
return None

View File

@@ -20,10 +20,8 @@
# 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))]
@@ -296,7 +294,7 @@ def dec_addr_tlv(hexstr):
elif addr_type == 0x01: # IPv4
# Skip address tye byte i.e. first byte in value list
# Skip the unused byte in Octet 4 after address type byte as per 3GPP TS 31.102
# 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')
@@ -332,82 +330,3 @@ def enc_addr_tlv(addr, addr_type='00'):
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,128 +0,0 @@
# -*- coding: utf-8 -*-
""" pySim: Logging
"""
#
# (C) 2025 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier <pmaier@sysmocom.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
from cmd2 import style
class _PySimLogHandler(logging.Handler):
def __init__(self, log_callback):
super().__init__()
self.log_callback = log_callback
def emit(self, record):
formatted_message = self.format(record)
self.log_callback(formatted_message, record)
class PySimLogger:
"""
Static class to centralize the log output of PySim applications. This class can be used to print log messages from
any pySim module. Configuration of the log behaviour (see setup and set_ methods) is entirely optional. In case no
print callback is set (see setup method), the logger will pass the log messages directly to print() without applying
any formatting to the original log message.
"""
LOG_FMTSTR = "%(levelname)s: %(message)s"
LOG_FMTSTR_VERBOSE = "%(module)s.%(lineno)d -- " + LOG_FMTSTR
__formatter = logging.Formatter(LOG_FMTSTR)
__formatter_verbose = logging.Formatter(LOG_FMTSTR_VERBOSE)
# No print callback by default, means that log messages are passed directly to print()
print_callback = None
# No specific color scheme by default
colors = {}
# The logging default is non-verbose logging on logging level DEBUG. This is a safe default that works for
# applications that ignore the presence of the PySimLogger class.
verbose = False
logging.root.setLevel(logging.DEBUG)
def __init__(self):
raise RuntimeError('static class, do not instantiate')
@staticmethod
def setup(print_callback = None, colors:dict = {}):
"""
Set a print callback function and color scheme. This function call is optional. In case this method is not
called, default settings apply.
Args:
print_callback : A callback function that accepts the resulting log string as input. The callback should
have the following format: print_callback(message:str)
colors : An optional dict through which certain log levels can be assigned a color.
(e.g. {logging.WARN: YELLOW})
"""
PySimLogger.print_callback = print_callback
PySimLogger.colors = colors
@staticmethod
def set_verbose(verbose:bool = False):
"""
Enable/disable verbose logging. (has no effect in case no print callback is set, see method setup)
Args:
verbose: verbosity (True = verbose logging, False = normal logging)
"""
PySimLogger.verbose = verbose;
@staticmethod
def set_level(level:int = logging.DEBUG):
"""
Set the logging level.
Args:
level: Logging level, valis log leves are: DEBUG, INFO, WARNING, ERROR and CRITICAL
"""
logging.root.setLevel(level)
@staticmethod
def _log_callback(message, record):
if not PySimLogger.print_callback:
# In case no print callback has been set display the message as if it were printed trough a normal
# python print statement.
print(record.message)
else:
# When a print callback is set, use it to display the log line. Apply color if the API user chose one
if PySimLogger.verbose:
formatted_message = logging.Formatter.format(PySimLogger.__formatter_verbose, record)
else:
formatted_message = logging.Formatter.format(PySimLogger.__formatter, record)
color = PySimLogger.colors.get(record.levelno)
if color:
if isinstance(color, str):
PySimLogger.print_callback(color + formatted_message + "\033[0m")
else:
PySimLogger.print_callback(style(formatted_message, fg = color))
else:
PySimLogger.print_callback(formatted_message)
@staticmethod
def get(log_facility: str):
"""
Set up and return a new python logger object
Args:
log_facility : Name of log facility (e.g. "MAIN", "RUNTIME"...)
"""
logger = logging.getLogger(log_facility)
handler = _PySimLogHandler(log_callback=PySimLogger._log_callback)
logger.addHandler(handler)
return logger

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-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
@@ -15,16 +15,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pySim.construct import *
from pySim.utils import b2h
from pySim.sms import UserDataHeader
from construct import *
import zlib
import abc
import struct
from typing import Optional, Tuple
from construct import Enum, Int8ub, Int16ub, Struct, 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
@@ -57,13 +55,12 @@ CompactRemoteResp = Struct('number_of_commands'/Int8ub,
'last_response_data'/HexAdapter(GreedyBytes))
RC_CC_DS = Enum(BitsInteger(2), no_rc_cc_ds=0, rc=1, cc=2, ds=3)
CNTR_REQ = Enum(BitsInteger(2), no_counter=0, counter_no_replay_or_seq=1, counter_must_be_higher=2, counter_must_be_lower=3)
POR_REQ = Enum(BitsInteger(2), no_por=0, por_required=1, por_only_when_error=2)
# TS 102 225 Section 5.1.1 + TS 31.115 Section 4.2
SPI = BitStruct( # first octet
Padding(3),
'counter'/CNTR_REQ,
'counter'/Enum(BitsInteger(2), no_counter=0, counter_no_replay_or_seq=1,
counter_must_be_higher=2, counter_must_be_lower=3),
'ciphering'/Flag,
'rc_cc_ds'/RC_CC_DS,
# second octet
@@ -71,7 +68,8 @@ SPI = BitStruct( # first octet
'por_in_submit'/Flag,
'por_shall_be_ciphered'/Flag,
'por_rc_cc_ds'/RC_CC_DS,
'por'/POR_REQ
'por'/Enum(BitsInteger(2), no_por=0,
por_required=1, por_only_when_error=2)
)
# TS 102 225 Section 5.1.2
@@ -99,17 +97,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 +112,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,24 +128,26 @@ 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
@@ -201,7 +190,7 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
def encrypt(self, data:bytes) -> bytes:
"""Encrypt given input bytes using the key material given in constructor."""
padded_data = self.pad_to_blocksize(data)
return self._encrypt(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 +199,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 +239,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:
@@ -333,7 +324,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 +334,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 +344,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,55 +387,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:
cpl = None # FIXME this line was just added to silence pylint possibly-used-before-assignment
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)
@@ -456,7 +399,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
@@ -493,7 +436,7 @@ class OtaDialectSms(OtaDialect):
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

@@ -4,7 +4,7 @@
"""
#
# (C) 2021 by sysmocom - s.f.m.c. GmbH
# (C) 2021 by Sysmocom s.f.m.c. GmbH
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
@@ -21,14 +21,57 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from pySim.commands import SimCardCommands
from pySim.filesystem import CardApplication, interpret_sw
from pySim.utils import all_subclasses
import abc
import operator
from typing import List
from pySim.exceptions import SwMatchError
from pySim.commands import SimCardCommands
from pySim.filesystem import CardApplication, interpret_sw
from pySim.utils import all_subclasses
def _mf_select_test(scc: SimCardCommands,
cla_byte: str, sel_ctrl: str,
fids: List[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:
for fid in fids:
scc.select_file(fid)
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", ["3f00"])
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", ["3f00"])
def match_ruim(scc: SimCardCommands) -> bool:
""" 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.
"""
return _mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
class CardProfile:
"""A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
@@ -95,36 +138,10 @@ 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
placeholder that is overloaded by specific derived 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
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
@@ -135,17 +152,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):
@@ -159,7 +166,7 @@ class CardProfile:
return None
def add_addon(self, addon: 'CardProfileAddon'):
assert addon not in self.addons
assert(addon not in self.addons)
# we don't install any additional files, as that is happening in the RuntimeState.
self.addons.append(addon)
@@ -179,6 +186,7 @@ class CardProfileAddon(abc.ABC):
self.desc = kw.get("desc", None)
self.files_in_mf = kw.get("files_in_mf", [])
self.shell_cmdsets = kw.get("shell_cmdsets", [])
pass
def __str__(self):
return self.name
@@ -186,3 +194,4 @@ class CardProfileAddon(abc.ABC):
@abc.abstractmethod
def probe(self, card: 'CardBase') -> bool:
"""Probe a given card to determine whether or not this add-on is present/supported."""
pass

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