mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-24 14:28:32 +03:00
Compare commits
1 Commits
pmaier/pgs
...
laforge/sm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bef85dbc28 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
|||||||
open_collective: osmocom
|
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -3,14 +3,3 @@
|
|||||||
|
|
||||||
/docs/_*
|
/docs/_*
|
||||||
/docs/generated
|
/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
|
|
||||||
|
|||||||
53
README.md
53
README.md
@@ -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
|
This repository contains a number of Python programs that can be used
|
||||||
subscriber identity modules of cellular networks, including but not limited
|
to read, program (write) and browse all fields/parameters/files on
|
||||||
to SIM, UICC, USIM, ISIM, HPSIMs and eUICCs.
|
SIM/USIM/ISIM/HPSIM cards used in 3GPP cellular networks from 2G to 5G.
|
||||||
|
|
||||||
* `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.
|
|
||||||
|
|
||||||
Note that the access control configuration of normal production cards
|
Note that the access control configuration of normal production cards
|
||||||
issue by operators will restrict significantly which files a normal
|
issue by operators will restrict significantly which files a normal
|
||||||
user can read, and particularly write to.
|
user can read, and particularly write to.
|
||||||
|
|
||||||
The full functionality of pySim hence can only be used with on so-called
|
The full functionality of pySim hence can only be used with on so-called
|
||||||
programmable SIM/USIM/ISIM/HPSIM cards, such as the various
|
programmable SIM/USIM/ISIM/HPSIM cards.
|
||||||
[sysmocom programmable card products](https://shop.sysmocom.de/SIM/).
|
|
||||||
|
|
||||||
Such SIM/USIM/ISIM/HPSIM cards are special cards, which - unlike those
|
Such SIM/USIM/ISIM/HPSIM cards are special cards, which - unlike those
|
||||||
issued by regular commercial operators - come with the kind of keys that
|
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
|
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
|
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
|
they have very specific requirements like batch programming of large
|
||||||
quantities of cards, which is about the only remaining use case for the
|
quantities of cards, which is about the only remaining use case for the
|
||||||
legacy tools.
|
legacy tools.
|
||||||
@@ -90,7 +77,7 @@ Please install the following dependencies:
|
|||||||
- cmd2 >= 1.5.0
|
- cmd2 >= 1.5.0
|
||||||
- colorlog
|
- colorlog
|
||||||
- construct >= 2.9.51
|
- construct >= 2.9.51
|
||||||
- pyosmocom
|
- gsm0338
|
||||||
- jsonpath-ng
|
- jsonpath-ng
|
||||||
- packaging
|
- packaging
|
||||||
- pycryptodomex
|
- pycryptodomex
|
||||||
@@ -100,7 +87,6 @@ Please install the following dependencies:
|
|||||||
- pyyaml >= 5.1
|
- pyyaml >= 5.1
|
||||||
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
|
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
|
||||||
- termcolor
|
- termcolor
|
||||||
- psycopg2-binary
|
|
||||||
|
|
||||||
Example for Debian:
|
Example for Debian:
|
||||||
```sh
|
```sh
|
||||||
@@ -137,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
|
Mailing List
|
||||||
------------
|
------------
|
||||||
|
|
||||||
There is no separate mailing list for this project. However,
|
There is no separate mailing list for this project. However,
|
||||||
discussions related to pySim are happening on the simtrace
|
discussions related to pysim-prog are happening on the
|
||||||
<simtrace@lists.osmocom.org> mailing list, please see
|
<openbsc@lists.osmocom.org> mailing list, please see
|
||||||
<https://lists.osmocom.org/mailman/listinfo/simtrace> for subscription
|
<https://lists.osmocom.org/mailman/listinfo/openbsc> for subscription
|
||||||
options and the list archive.
|
options and the list archive.
|
||||||
|
|
||||||
Please observe the [Osmocom Mailing List
|
Please observe the [Osmocom Mailing List
|
||||||
Rules](https://osmocom.org/projects/cellular-infrastructure/wiki/Mailing_List_Rules)
|
Rules](https://osmocom.org/projects/cellular-infrastructure/wiki/Mailing_List_Rules)
|
||||||
when posting.
|
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
|
Contributing
|
||||||
------------
|
------------
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import csv
|
|
||||||
import sys
|
|
||||||
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("CSV2PGQSL")
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
log = PySimLogger.get("PQSQL")
|
|
||||||
self.table = table_name
|
|
||||||
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.lower()),
|
|
||||||
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.lower()),
|
|
||||||
Identifier(c.lower())))
|
|
||||||
|
|
||||||
# Set permissions
|
|
||||||
self.cur.execute(SQL("GRANT INSERT ON {} TO {};").format(Identifier(self.table.lower()),
|
|
||||||
Identifier(user_importer)))
|
|
||||||
self.cur.execute(SQL("GRANT SELECT ON {} TO {};").format(Identifier(self.table.lower()),
|
|
||||||
Identifier(user_reader)))
|
|
||||||
|
|
||||||
log.info("New database table created: %s", str(self.table.lower()))
|
|
||||||
|
|
||||||
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.lower(),))
|
|
||||||
|
|
||||||
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.lower()),
|
|
||||||
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.lower()))
|
|
||||||
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(opts.pqsql, 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('--pqsql', metavar='FILE',
|
|
||||||
default=str(Path.home()) + "/.osmocom/pysim/card_data_pqsql.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()
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Command line tool to compute or verify EID (eUICC ID) values
|
|
||||||
#
|
|
||||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from pySim.euicc import compute_eid_checksum, verify_eid_checksum
|
|
||||||
|
|
||||||
|
|
||||||
option_parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
description="""pySim EID Tool
|
|
||||||
This utility program can be used to compute or verify the checksum of an EID
|
|
||||||
(eUICC Identifier). See GSMA SGP.29 for the algorithm details.
|
|
||||||
|
|
||||||
Example (verification):
|
|
||||||
$ eidtool.py --verify 89882119900000000000000000001654
|
|
||||||
EID checksum verified successfully
|
|
||||||
|
|
||||||
Example (generation, passing first 30 digits):
|
|
||||||
$ eidtool.py --compute 898821199000000000000000000016
|
|
||||||
89882119900000000000000000001654
|
|
||||||
|
|
||||||
Example (generation, passing all 32 digits):
|
|
||||||
$ eidtool.py --compute 89882119900000000000000000001600
|
|
||||||
89882119900000000000000000001654
|
|
||||||
|
|
||||||
Example (generation, specifying base 30 digits and number to add):
|
|
||||||
$ eidtool.py --compute 898821199000000000000000000000 --add 16
|
|
||||||
89882119900000000000000000001654
|
|
||||||
""")
|
|
||||||
group = option_parser.add_mutually_exclusive_group(required=True)
|
|
||||||
group.add_argument('--verify', help='Verify given EID csum')
|
|
||||||
group.add_argument('--compute', help='Generate EID csum')
|
|
||||||
option_parser.add_argument('--add', type=int, help='Add value to EID base before computing')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
opts = option_parser.parse_args()
|
|
||||||
|
|
||||||
if opts.verify:
|
|
||||||
res = verify_eid_checksum(opts.verify)
|
|
||||||
if res:
|
|
||||||
print("EID checksum verified successfully")
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
print("EID checksum invalid")
|
|
||||||
sys.exit(1)
|
|
||||||
elif opts.compute:
|
|
||||||
eid = opts.compute
|
|
||||||
if opts.add:
|
|
||||||
if len(eid) != 30:
|
|
||||||
print("EID base must be 30 digits when using --add")
|
|
||||||
sys.exit(2)
|
|
||||||
eid = str(int(eid) + int(opts.add))
|
|
||||||
res = compute_eid_checksum(eid)
|
|
||||||
print(res)
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
@@ -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 == 'download':
|
|
||||||
pird = {
|
|
||||||
'transactionId': self.opts.transaction_id,
|
|
||||||
'notificationMetadata': ntf_metadata,
|
|
||||||
'smdpOid': self.opts.smdpp_oid,
|
|
||||||
'finalResult': ('successResult', {
|
|
||||||
'aid': self.opts.isdp_aid,
|
|
||||||
'simaResponse': self.opts.sima_response,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
|
|
||||||
signature = self.cert_and_key.ecdsa_sign(pird_bin)
|
|
||||||
pn_dict = ('profileInstallationResult', {
|
|
||||||
'profileInstallationResultData': pird,
|
|
||||||
'euiccSignPIR': signature,
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
ntf_bin = rsp.asn1.encode('NotificationMetadata', ntf_metadata)
|
|
||||||
signature = self.cert_and_key.ecdsa_sign(ntf_bin)
|
|
||||||
pn_dict = ('otherSignedNotification', {
|
|
||||||
'tbsOtherNotification': ntf_metadata,
|
|
||||||
'euiccNotificationSignature': signature,
|
|
||||||
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
|
|
||||||
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER)),
|
|
||||||
})
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'pendingNotification': pn_dict,
|
|
||||||
}
|
|
||||||
#print(data)
|
|
||||||
res = self.peer.call_handleNotification(data)
|
|
||||||
|
|
||||||
|
|
||||||
def do_download(self):
|
|
||||||
|
|
||||||
print("Step 1: InitiateAuthentication...")
|
|
||||||
|
|
||||||
euiccInfo1 = {
|
|
||||||
'svn': b'\x02\x04\x00',
|
|
||||||
'euiccCiPKIdListForVerification': [
|
|
||||||
self.ci_pkid,
|
|
||||||
],
|
|
||||||
'euiccCiPKIdListForSigning': [
|
|
||||||
self.ci_pkid,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'euiccChallenge': os.urandom(16),
|
|
||||||
'euiccInfo1': euiccInfo1,
|
|
||||||
'smdpAddress': urlparse(self.opts.url).netloc,
|
|
||||||
}
|
|
||||||
init_auth_res = self.peer.call_initiateAuthentication(data)
|
|
||||||
print(init_auth_res)
|
|
||||||
|
|
||||||
print("Step 2: AuthenticateClient...")
|
|
||||||
|
|
||||||
#res['serverSigned1']
|
|
||||||
#res['serverSignature1']
|
|
||||||
print("TODO: verify serverSignature1 over serverSigned1")
|
|
||||||
#res['transactionId']
|
|
||||||
print("TODO: verify transactionId matches the signed one in serverSigned1")
|
|
||||||
#res['euiccCiPKIdToBeUsed']
|
|
||||||
# TODO: select eUICC certificate based on CI
|
|
||||||
#res['serverCertificate']
|
|
||||||
# TODO: verify server certificate against CI
|
|
||||||
|
|
||||||
euiccInfo2 = {
|
|
||||||
'profileVersion': b'\x02\x03\x01',
|
|
||||||
'svn': euiccInfo1['svn'],
|
|
||||||
'euiccFirmwareVer': b'\x23\x42\x00',
|
|
||||||
'extCardResource': b'\x81\x01\x00\x82\x04\x00\x04\x9ch\x83\x02"#',
|
|
||||||
'uiccCapability': (b'k6\xd3\xc3', 32),
|
|
||||||
'javacardVersion': b'\x11\x02\x00',
|
|
||||||
'globalplatformVersion': b'\x02\x03\x00',
|
|
||||||
'rspCapability': (b'\x9c', 6),
|
|
||||||
'euiccCiPKIdListForVerification': euiccInfo1['euiccCiPKIdListForVerification'],
|
|
||||||
'euiccCiPKIdListForSigning': euiccInfo1['euiccCiPKIdListForSigning'],
|
|
||||||
#'euiccCategory':
|
|
||||||
#'forbiddenProfilePolicyRules':
|
|
||||||
'ppVersion': b'\x01\x00\x00',
|
|
||||||
'sasAcreditationNumber': 'OSMOCOM-TEST-1', #TODO: make configurable
|
|
||||||
#'certificationDataObject':
|
|
||||||
}
|
|
||||||
|
|
||||||
euiccSigned1 = {
|
|
||||||
'transactionId': h2b(init_auth_res['transactionId']),
|
|
||||||
'serverAddress': init_auth_res['serverSigned1']['serverAddress'],
|
|
||||||
'serverChallenge': init_auth_res['serverSigned1']['serverChallenge'],
|
|
||||||
'euiccInfo2': euiccInfo2,
|
|
||||||
'ctxParams1':
|
|
||||||
('ctxParamsForCommonAuthentication', {
|
|
||||||
'matchingId': self.opts.matchingId,
|
|
||||||
'deviceInfo': {
|
|
||||||
'tac': b'\x35\x23\x01\x45', # same as lpac
|
|
||||||
'deviceCapabilities': {},
|
|
||||||
#imei:
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
|
|
||||||
euiccSignature1 = self.cert_and_key.ecdsa_sign(euiccSigned1_bin)
|
|
||||||
auth_clnt_req = {
|
|
||||||
'transactionId': init_auth_res['transactionId'],
|
|
||||||
'authenticateServerResponse':
|
|
||||||
('authenticateResponseOk', {
|
|
||||||
'euiccSigned1': euiccSigned1,
|
|
||||||
'euiccSignature1': euiccSignature1,
|
|
||||||
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
|
|
||||||
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
auth_clnt_res = self.peer.call_authenticateClient(auth_clnt_req)
|
|
||||||
print(auth_clnt_res)
|
|
||||||
#auth_clnt_res['transactionId']
|
|
||||||
print("TODO: verify transactionId matches previous ones")
|
|
||||||
#auth_clnt_res['profileMetadata']
|
|
||||||
# TODO: what's in here?
|
|
||||||
#auth_clnt_res['smdpSigned2']['bppEuiccOtpk']
|
|
||||||
#auth_clnt_res['smdpSignature2']
|
|
||||||
print("TODO: verify serverSignature2 over smdpSigned2")
|
|
||||||
|
|
||||||
smdp_cert = x509.load_der_x509_certificate(auth_clnt_res['smdpCertificate'])
|
|
||||||
|
|
||||||
print("Step 3: GetBoundProfilePackage...")
|
|
||||||
# Generate a one-time ECKA key pair (ot{PK,SK}.DP.ECKA) using the curve indicated by the Key Parameter
|
|
||||||
# Reference value of CERT.DPpb.ECDSA
|
|
||||||
euicc_ot = ec.generate_private_key(smdp_cert.public_key().public_numbers().curve)
|
|
||||||
|
|
||||||
# extract the public key in (hopefully) the right format for the ES8+ interface
|
|
||||||
euicc_otpk = euicc_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
|
|
||||||
|
|
||||||
euiccSigned2 = {
|
|
||||||
'transactionId': h2b(auth_clnt_res['transactionId']),
|
|
||||||
'euiccOtpk': euicc_otpk,
|
|
||||||
#hashCC
|
|
||||||
}
|
|
||||||
# check for smdpSigned2 ccRequiredFlag, and send it in PrepareDownloadRequest hashCc
|
|
||||||
if auth_clnt_res['smdpSigned2']['ccRequiredFlag']:
|
|
||||||
if not self.opts.confirmation_code:
|
|
||||||
raise ValueError('Confirmation Code required but not provided')
|
|
||||||
cc_hash = hashlib.sha256(self.opts.confirmation_code.encode('ascii')).digest()
|
|
||||||
euiccSigned2['hashCc'] = hashlib.sha256(cc_hash + euiccSigned2['transactionId']).digest()
|
|
||||||
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
|
|
||||||
euiccSignature2 = self.cert_and_key.ecdsa_sign(euiccSigned2_bin + auth_clnt_res['smdpSignature2'])
|
|
||||||
gbp_req = {
|
|
||||||
'transactionId': auth_clnt_res['transactionId'],
|
|
||||||
'prepareDownloadResponse':
|
|
||||||
('downloadResponseOk', {
|
|
||||||
'euiccSigned2': euiccSigned2,
|
|
||||||
'euiccSignature2': euiccSignature2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
gbp_res = self.peer.call_getBoundProfilePackage(gbp_req)
|
|
||||||
print(gbp_res)
|
|
||||||
#gbp_res['transactionId']
|
|
||||||
# TODO: verify transactionId
|
|
||||||
print("TODO: verify transactionId matches previous ones")
|
|
||||||
bpp_bin = gbp_res['boundProfilePackage']
|
|
||||||
print("TODO: verify boundProfilePackage smdpSignature")
|
|
||||||
|
|
||||||
bpp = BoundProfilePackage()
|
|
||||||
upp_bin = bpp.decode(euicc_ot, self.eid, bpp_bin)
|
|
||||||
|
|
||||||
iccid = swap_nibbles(b2h(bpp.storeMetadataRequest['iccid']))
|
|
||||||
base_name = os.path.join(self.opts.output_path, '%s' % iccid)
|
|
||||||
|
|
||||||
print("SUCCESS: Storing files as %s.*.der" % base_name)
|
|
||||||
|
|
||||||
# write various output files
|
|
||||||
with open(base_name+'.upp.der', 'wb') as f:
|
|
||||||
f.write(bpp.upp)
|
|
||||||
with open(base_name+'.isdp.der', 'wb') as f:
|
|
||||||
f.write(bpp.encoded_configureISDPRequest)
|
|
||||||
with open(base_name+'.smr.der', 'wb') as f:
|
|
||||||
f.write(bpp.encoded_storeMetadataRequest)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
opts = parser.parse_args()
|
|
||||||
|
|
||||||
c = Es9pClient(opts)
|
|
||||||
|
|
||||||
if opts.command == 'download':
|
|
||||||
c.do_download()
|
|
||||||
elif opts.command == 'notification':
|
|
||||||
c.do_notification()
|
|
||||||
elif opts.command == 'notification-install':
|
|
||||||
opts.operation = 'install'
|
|
||||||
c.do_notification()
|
|
||||||
@@ -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))
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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()
|
|
||||||
@@ -4,23 +4,18 @@
|
|||||||
# environment variables:
|
# environment variables:
|
||||||
# * WITH_MANUALS: build manual PDFs if set to "1"
|
# * WITH_MANUALS: build manual PDFs if set to "1"
|
||||||
# * PUBLISH: upload manuals after building if set to "1" (ignored without WITH_MANUALS = "1")
|
# * PUBLISH: upload manuals after building if set to "1" (ignored without WITH_MANUALS = "1")
|
||||||
# * JOB_TYPE: one of 'test', 'distcheck', 'pylint', 'docs'
|
# * JOB_TYPE: one of 'test', 'pylint', 'docs'
|
||||||
# * SKIP_CLEAN_WORKSPACE: don't run osmo-clean-workspace.sh (for pyosmocom CI)
|
|
||||||
#
|
#
|
||||||
|
|
||||||
export PYTHONUNBUFFERED=1
|
export PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
if [ ! -d "./tests/" ] ; then
|
if [ ! -d "./pysim-testdata/" ] ; then
|
||||||
echo "###############################################"
|
echo "###############################################"
|
||||||
echo "Please call from pySim-prog top directory"
|
echo "Please call from pySim-prog top directory"
|
||||||
echo "###############################################"
|
echo "###############################################"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$SKIP_CLEAN_WORKSPACE" ]; then
|
|
||||||
osmo-clean-workspace.sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$JOB_TYPE" in
|
case "$JOB_TYPE" in
|
||||||
"test")
|
"test")
|
||||||
virtualenv -p python3 venv --system-site-packages
|
virtualenv -p python3 venv --system-site-packages
|
||||||
@@ -30,39 +25,16 @@ case "$JOB_TYPE" in
|
|||||||
pip install pyshark
|
pip install pyshark
|
||||||
|
|
||||||
# Execute automatically discovered unit tests first
|
# 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)
|
# Run the test with physical cards
|
||||||
cd tests/pySim-prog_test/
|
cd pysim-testdata
|
||||||
./pySim-prog_test.sh
|
../tests/pySim-prog_test.sh
|
||||||
cd ../../
|
../tests/pySim-trace_test.sh
|
||||||
|
|
||||||
# Run pySim-trace test
|
|
||||||
tests/pySim-trace_test/pySim-trace_test.sh
|
|
||||||
|
|
||||||
# Run pySim-shell integration tests (requires physical cards)
|
|
||||||
python3 -m unittest discover -v -s ./tests/pySim-shell_test/
|
|
||||||
;;
|
|
||||||
"distcheck")
|
|
||||||
virtualenv -p python3 venv --system-site-packages
|
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
pip install .
|
|
||||||
pip install pyshark
|
|
||||||
|
|
||||||
for prog in venv/bin/pySim-*.py; do
|
|
||||||
$prog --help > /dev/null
|
|
||||||
done
|
|
||||||
;;
|
;;
|
||||||
"pylint")
|
"pylint")
|
||||||
# Print pylint version
|
# Print pylint version
|
||||||
pip3 freeze | grep pylint
|
pip3 freeze | grep pylint
|
||||||
|
|
||||||
virtualenv -p python3 venv --system-site-packages
|
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
pip install .
|
|
||||||
|
|
||||||
# Run pylint to find potential errors
|
# Run pylint to find potential errors
|
||||||
# Ignore E1102: not-callable
|
# Ignore E1102: not-callable
|
||||||
# pySim/filesystem.py: E1102: method is not callable (not-callable)
|
# pySim/filesystem.py: E1102: method is not callable (not-callable)
|
||||||
@@ -73,15 +45,9 @@ case "$JOB_TYPE" in
|
|||||||
--disable E1102 \
|
--disable E1102 \
|
||||||
--disable E0401 \
|
--disable E0401 \
|
||||||
--enable W0301 \
|
--enable W0301 \
|
||||||
pySim tests/unittests/*.py *.py \
|
pySim *.py
|
||||||
contrib/*.py
|
|
||||||
;;
|
;;
|
||||||
"docs")
|
"docs")
|
||||||
virtualenv -p python3 venv --system-site-packages
|
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
rm -rf docs/_build
|
rm -rf docs/_build
|
||||||
make -C "docs" html latexpdf
|
make -C "docs" html latexpdf
|
||||||
|
|
||||||
@@ -94,5 +60,3 @@ case "$JOB_TYPE" in
|
|||||||
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
|
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
|
||||||
exit 1
|
exit 1
|
||||||
esac
|
esac
|
||||||
|
|
||||||
osmo-clean-workspace.sh
|
|
||||||
|
|||||||
@@ -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_info = 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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -162,7 +162,6 @@ def main(argv):
|
|||||||
parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
|
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)
|
parser.add_argument("-n", "--slot-nr", help="SIM slot number", type=int, default=0)
|
||||||
subp = parser.add_subparsers()
|
subp = parser.add_subparsers()
|
||||||
subp.required = True
|
|
||||||
|
|
||||||
auth_p = subp.add_parser('auth', help='UMTS AKA Authentication')
|
auth_p = subp.add_parser('auth', help='UMTS AKA Authentication')
|
||||||
auth_p.add_argument("-c", "--count", help="Auth count", type=int, default=10)
|
auth_p.add_argument("-c", "--count", help="Auth count", type=int, default=10)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
# Give a hierarchical decode of BER/DER-encoded ASN.1 TLVs
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from osmocom.utils import b2h, h2b
|
from pySim.utils import bertlv_parse_one, bertlv_encode_tag, b2h, h2b
|
||||||
from osmocom.tlv import bertlv_parse_one, bertlv_encode_tag
|
|
||||||
|
|
||||||
def process_one_level(content: bytes, indent: int):
|
def process_one_level(content: bytes, indent: int):
|
||||||
remainder = content
|
remainder = content
|
||||||
@@ -36,8 +35,5 @@ if __name__ == '__main__':
|
|||||||
content = f.read()
|
content = f.read()
|
||||||
elif opts.hex:
|
elif opts.hex:
|
||||||
content = h2b(opts.hex)
|
content = h2b(opts.hex)
|
||||||
else:
|
|
||||||
# avoid pylint "(possibly-used-before-assignment)" below
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
process_one_level(content, 0)
|
process_one_level(content, 0)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# You can set these variables from the command line, and also
|
# You can set these variables from the command line, and also
|
||||||
# from the environment for the first two.
|
# from the environment for the first two.
|
||||||
SPHINXOPTS ?=
|
SPHINXOPTS ?=
|
||||||
SPHINXBUILD ?= python3 -m sphinx.cmd.build
|
SPHINXBUILD ?= sphinx-build
|
||||||
SOURCEDIR = .
|
SOURCEDIR = .
|
||||||
BUILDDIR = _build
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,333 +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 CardKeyProviderPqsql
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
With the `CardKeyProviderPsql` 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.
|
|
||||||
|
|
||||||
|
|
||||||
Setting up the database
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
From the perspective of the database, the `CardKeyProviderPsql` 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 `CardKeyProviderPsql` 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 `CardKeyProviderPsql`
|
|
||||||
|
|
||||||
* **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
|
|
||||||
`CardKeyProviderPsql`
|
|
||||||
|
|
||||||
* **reader**:
|
|
||||||
To access data from within pySim shell using the `CardKeyProviderPsql` 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_pqsql.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 `CardKeyProviderPsql` shall
|
|
||||||
use to query to locate card key data. You can set up as many tables as you
|
|
||||||
want, `CardKeyProviderPsql` 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_pqsql.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_pqsql.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_pqsql.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_pqsql.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.
|
|
||||||
10
docs/conf.py
10
docs/conf.py
@@ -18,17 +18,9 @@ sys.path.insert(0, os.path.abspath('..'))
|
|||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'osmopysim-usermanual'
|
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'
|
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 ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,8 @@ pySim consists of several parts:
|
|||||||
shell
|
shell
|
||||||
trace
|
trace
|
||||||
legacy
|
legacy
|
||||||
smpp2sim
|
|
||||||
library
|
library
|
||||||
library-esim
|
|
||||||
osmo-smdpp
|
osmo-smdpp
|
||||||
sim-rest
|
|
||||||
suci-keytool
|
|
||||||
saip-tool
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|||||||
194
docs/legacy.rst
194
docs/legacy.rst
@@ -1,20 +1,20 @@
|
|||||||
Legacy tools
|
Legacy tools
|
||||||
============
|
============
|
||||||
|
|
||||||
*legacy tools* are the classic ``pySim-prog`` and ``pySim-read`` programs that
|
*legacy tools* are the classic ``pySim-prog`` and ``pySim-read`` programs that
|
||||||
existed long before ``pySim-shell``.
|
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.
|
legacy tools.
|
||||||
|
|
||||||
pySim-prog
|
pySim-prog
|
||||||
----------
|
----------
|
||||||
|
|
||||||
``pySim-prog`` was the first part of the pySim software suite. It started as a
|
``pySim-prog`` was the first part of the pySim software suite. It started as
|
||||||
tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and was
|
a tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and
|
||||||
later extended to a variety of other cards. As the number of features supported
|
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`
|
became no longer bearable to express with command-line arguments, `pySim-shell` was
|
||||||
was created.
|
created.
|
||||||
|
|
||||||
Basic use cases can still use `pySim-prog`.
|
Basic use cases can still use `pySim-prog`.
|
||||||
|
|
||||||
@@ -22,180 +22,36 @@ Program customizable SIMs
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Two modes are possible:
|
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
|
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -i <IMSI> -s <ICCID>``
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
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
|
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -z <random_string_of_choice> -j <card_num>``
|
||||||
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.
|
|
||||||
|
|
||||||
A CSV files may hold rows for multiple (hundreds or even thousands) of
|
With <random_string_of_choice> and <card_num>, the soft will generate
|
||||||
cards. ``pySim-prog`` is able to identify the rows either by ICCID
|
'predictable' IMSI and ICCID, so make sure you choose them so as not to
|
||||||
(recommended as ICCIDs are normally not changed) or IMSI.
|
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,
|
You also need to enter some parameters to select the device :
|
||||||
here the same rules as for the commandline parameters apply. The column names
|
-t TYPE : type of card (supersim, magicsim, fakemagicsim or try 'auto')
|
||||||
match the command line options. The CSV file may also contain columns that are
|
-d DEV : Serial port device (default /dev/ttyUSB0)
|
||||||
unknown to pySim-prog, such as inventory numbers, nicknames or parameters that
|
-b BAUD : Baudrate (default 9600)
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
pySim-read
|
pySim-read
|
||||||
----------
|
----------
|
||||||
|
|
||||||
``pySim-read`` allows to read some of the most important data items from a SIM
|
``pySim-read`` allows you to read some data from a SIM card. It will only some files
|
||||||
card. This means it will only read some files of the card, and will only read
|
of the card, and will only read files accessible to a normal user (without any special authentication)
|
||||||
files accessible to a normal user (without any special authentication)
|
|
||||||
|
|
||||||
These days, it is recommended to use the ``export`` command of ``pySim-shell``
|
These days, you should use the ``export`` command of ``pySim-shell``
|
||||||
instead. It performs a much more comprehensive export of all of the [standard]
|
instead. It performs a much more comprehensive export of all of the
|
||||||
files that can be found on the card. To get a human-readable decode instead of
|
[standard] files that can be found on the card. To get a human-readable
|
||||||
the raw hex export, you can use ``export --json``.
|
decode instead of the raw hex export, you can use ``export --json``.
|
||||||
|
|
||||||
Specifically, pySim-read will dump the following:
|
Specifically, pySim-read will dump the following:
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
|
||||||
@@ -74,6 +74,18 @@ at 9600 bps. These readers are sometimes called `Phoenix`.
|
|||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
pySim construct utilities
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: pySim.construct
|
||||||
|
:members:
|
||||||
|
|
||||||
|
pySim TLV utilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: pySim.tlv
|
||||||
|
:members:
|
||||||
|
|
||||||
pySim utility functions
|
pySim utility functions
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -19,42 +19,29 @@ support for profile personalization yet.
|
|||||||
|
|
||||||
osmo-smdpp currently
|
osmo-smdpp currently
|
||||||
|
|
||||||
* [by default] uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your
|
* always provides the exact same profile to every request. The profile always has the same IMSI and
|
||||||
osmo-smdpp would be running at the host name `testsmdpplus1.example.com`. You can of course replace those
|
ICCID.
|
||||||
certificates with your own, whether SGP.26 derived or part of a *private root CA* setup with matching eUICCs.
|
|
||||||
* 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)
|
|
||||||
* **is absolutely insecure**, as it
|
* **is absolutely insecure**, as it
|
||||||
|
|
||||||
* does not perform all of the mandatory certificate verification (it checks the certificate chain, but not
|
* does not perform any certificate verification
|
||||||
the expiration dates nor any CRL)
|
* does not evaluate/consider any *Matching ID* or *Confirmation Code*
|
||||||
* does not evaluate/consider any *Confirmation Code*
|
* stores the sessions in an unencrypted _python shelve_ and is hence leaking one-time key materials
|
||||||
* stores the sessions in an unencrypted *python shelve* and is hence leaking one-time key materials
|
|
||||||
used for profile encryption and signing.
|
used for profile encryption and signing.
|
||||||
|
|
||||||
|
|
||||||
Running osmo-smdpp
|
Running osmo-smdpp
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
osmo-smdpp comes with built-in TLS support which is enabled by default. However, it is always possible to
|
osmo-smdpp does not have built-in TLS support as the used *twisted* framework appears to have
|
||||||
disable the built-in TLS support if needed.
|
problems when using the example elliptic curve certificates (both NIST and Brainpool) from GSMA.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
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
|
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 {
|
upstream smdpp {
|
||||||
server localhost:8000;
|
server localhost:8000;
|
||||||
@@ -84,66 +71,23 @@ 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
|
You can of course achieve a similar functionality with apache, lighttpd or many other web server
|
||||||
software.
|
software.
|
||||||
|
|
||||||
supplementary files
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The `smdpp-data/certs` directory contains the DPtls, DPauth and DPpb as well as CI certificates
|
osmo-smdpp
|
||||||
used; they are copied from GSMA SGP.26 v2. You can of course replace them with custom certificates
|
~~~~~~~~~~
|
||||||
if you're operating eSIM with a *private root CA*.
|
|
||||||
|
|
||||||
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used. The file names (without
|
osmo-smdpp currently doesn't have any configuration file or command line options. You just run it,
|
||||||
.der suffix) are looked up by the matchingID parameter from the activation code presented by the LPA.
|
and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
|
||||||
|
|
||||||
commandline options
|
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.
|
||||||
|
|
||||||
Typically, you just run osmo-smdpp without any arguments, and it will bind its built-in HTTPS ES9+ interface to
|
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used.
|
||||||
`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
|
DNS setup for your LPA
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The LPA must resolve `testsmdpplus1.example.com` to the IP address of your TLS proxy.
|
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 must also accept the TLS certificates used by your TLS proxy.
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
Supported eUICC
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
If you run osmo-smdpp with the included SGP.26 (DPauth, DPpb) certificates, you must use an eUICC with matching SGP.26
|
|
||||||
certificates, i.e. the EUM certificate must be signed by a SGP.26 test root CA and the eUICC certificate
|
|
||||||
in turn must be signed by that SGP.26 EUM certificate.
|
|
||||||
|
|
||||||
sysmocom (sponsoring development and maintenance of pySim and osmo-smdpp) is selling SGP.26 test eUICC
|
|
||||||
as `sysmoEUICC1-C2T`. They are publicly sold in the `sysmocom webshop <https://shop.sysmocom.de/eUICC-for-consumer-eSIM-RSP-with-SGP.26-Test-Certificates/sysmoEUICC1-C2T>`_.
|
|
||||||
|
|
||||||
In general you can use osmo-smdpp also with certificates signed by any other certificate authority. You
|
|
||||||
just always must ensure that the certificates of the SM-DP+ are signed by the same root CA as those of your
|
|
||||||
eUICCs.
|
|
||||||
|
|
||||||
Hypothetically, osmo-smdpp could also be operated with GSMA production certificates, but it would require
|
|
||||||
that somebody brings the code in-line with all the GSMA security requirements (HSM support, ...) and operate
|
|
||||||
it in a GSMA SAS-SM accredited environment and pays for the related audits.
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
216
docs/shell.rst
216
docs/shell.rst
@@ -1,4 +1,4 @@
|
|||||||
pySim-shell
|
pySim-shell
|
||||||
===========
|
===========
|
||||||
|
|
||||||
pySim-shell is an interactive command line shell for all kind of interactions with SIM cards,
|
pySim-shell is an interactive command line shell for all kind of interactions with SIM cards,
|
||||||
@@ -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
|
* if your card supports it, and you have the related privileges: resizing, creating, enabling and disabling of
|
||||||
files
|
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:
|
By means of using the python ``cmd2`` module, various useful features improve usability:
|
||||||
|
|
||||||
@@ -67,18 +64,8 @@ Usage Examples
|
|||||||
:caption: Tutorials for pySIM-shell:
|
:caption: Tutorials for pySIM-shell:
|
||||||
|
|
||||||
suci-tutorial
|
suci-tutorial
|
||||||
cap-tutorial
|
|
||||||
|
|
||||||
|
|
||||||
Advanced Topics
|
|
||||||
---------------
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
:caption: Advanced pySIM-shell topics
|
|
||||||
|
|
||||||
card-key-provider
|
|
||||||
remote-access
|
|
||||||
|
|
||||||
cmd2 basics
|
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.
|
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
|
tree
|
||||||
~~~~
|
~~~~
|
||||||
Display a tree of the card filesystem. It is important to note that this displays a tree
|
Display a tree of the card filesystem. It is important to note that this displays a tree
|
||||||
@@ -439,19 +400,7 @@ verify_chv
|
|||||||
|
|
||||||
deactivate_file
|
deactivate_file
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
Deactivate the currently selected file. A deactivated file can no longer be accessed
|
Deactivate the currently selected file. This used to be called INVALIDATE in TS 11.11.
|
||||||
for any further operation (such as selecting and subsequently reading or writing).
|
|
||||||
|
|
||||||
Any access to a file that is deactivated will trigger the error
|
|
||||||
*SW 6283 'Selected file invalidated/disabled'*
|
|
||||||
|
|
||||||
In order to re-access a deactivated file, you need to activate it again, see the
|
|
||||||
`activate_file` command below. Note that for *deactivation* the to-be-deactivated
|
|
||||||
EF must be selected, but for *activation*, the DF above the to-be-activated
|
|
||||||
EF must be selected!
|
|
||||||
|
|
||||||
This command sends a DEACTIVATE FILE APDU to
|
|
||||||
the card (used to be called INVALIDATE in TS 11.11 for classic SIM).
|
|
||||||
|
|
||||||
|
|
||||||
activate_file
|
activate_file
|
||||||
@@ -512,18 +461,7 @@ sequence including the electrical power down.
|
|||||||
:module: pySim.ts_102_221
|
:module: pySim.ts_102_221
|
||||||
:func: CardProfileUICC.AddlShellCommands.resume_uicc_parser
|
: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
|
Linear Fixed EF commands
|
||||||
@@ -544,9 +482,6 @@ read_record_decoded
|
|||||||
:module: pySim.filesystem
|
:module: pySim.filesystem
|
||||||
:func: LinFixedEF.ShellCommands.read_rec_dec_parser
|
: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
|
read_records
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
@@ -561,9 +496,6 @@ read_records_decoded
|
|||||||
:module: pySim.filesystem
|
:module: pySim.filesystem
|
||||||
:func: LinFixedEF.ShellCommands.read_recs_dec_parser
|
: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
|
update_record
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
@@ -578,9 +510,6 @@ update_record_decoded
|
|||||||
:module: pySim.filesystem
|
:module: pySim.filesystem
|
||||||
:func: LinFixedEF.ShellCommands.upd_rec_dec_parser
|
: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
|
edit_record_decoded
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -599,12 +528,6 @@ back to the record on the SIM card.
|
|||||||
|
|
||||||
This allows for easy interactive modification of records.
|
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
|
decode_hex
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
@@ -633,8 +556,6 @@ read_binary_decoded
|
|||||||
:module: pySim.filesystem
|
:module: pySim.filesystem
|
||||||
:func: TransparentEF.ShellCommands.read_bin_dec_parser
|
: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
|
update_binary
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
@@ -688,10 +609,6 @@ The below example demonstrates this by modifying the ciphering indicator field w
|
|||||||
"extensions": "ff"
|
"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
|
edit_binary_decoded
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
This command will read the selected binary EF, decode it to its JSON representation, save
|
This command will read the selected binary EF, decode it to its JSON representation, save
|
||||||
@@ -705,12 +622,6 @@ to the SIM card.
|
|||||||
|
|
||||||
This allows for easy interactive modification of file contents.
|
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
|
decode_hex
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
@@ -1003,25 +914,7 @@ aram_delete_all
|
|||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
This command will request deletion of all access rules stored within the
|
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
|
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`
|
intended must be manually inserted again using `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.
|
|
||||||
|
|
||||||
|
|
||||||
GlobalPlatform commands
|
GlobalPlatform commands
|
||||||
@@ -1036,88 +929,6 @@ get_data
|
|||||||
:module: pySim.global_platform
|
:module: pySim.global_platform
|
||||||
:func: ADF_SD.AddlShellCommands.get_data_parser
|
:func: ADF_SD.AddlShellCommands.get_data_parser
|
||||||
|
|
||||||
get_status
|
|
||||||
~~~~~~~~~~
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.global_platform
|
|
||||||
:func: ADF_SD.AddlShellCommands.get_status_parser
|
|
||||||
|
|
||||||
set_status
|
|
||||||
~~~~~~~~~~
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.global_platform
|
|
||||||
:func: ADF_SD.AddlShellCommands.set_status_parser
|
|
||||||
|
|
||||||
store_data
|
|
||||||
~~~~~~~~~~
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.global_platform
|
|
||||||
:func: ADF_SD.AddlShellCommands.store_data_parser
|
|
||||||
|
|
||||||
put_key
|
|
||||||
~~~~~~~
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.global_platform
|
|
||||||
:func: ADF_SD.AddlShellCommands.put_key_parser
|
|
||||||
|
|
||||||
delete_key
|
|
||||||
~~~~~~~~~~
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.global_platform
|
|
||||||
:func: ADF_SD.AddlShellCommands.del_key_parser
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
delete_card_content
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.global_platform
|
|
||||||
:func: ADF_SD.AddlShellCommands.del_cc_parser
|
|
||||||
|
|
||||||
establish_scp02
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.global_platform
|
|
||||||
:func: ADF_SD.AddlShellCommands.est_scp02_parser
|
|
||||||
|
|
||||||
establish_scp03
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.global_platform
|
|
||||||
:func: ADF_SD.AddlShellCommands.est_scp03_parser
|
|
||||||
|
|
||||||
release_scp
|
|
||||||
~~~~~~~~~~~
|
|
||||||
Release any previously established SCP (Secure Channel Protocol)
|
|
||||||
|
|
||||||
|
|
||||||
eUICC ISD-R commands
|
eUICC ISD-R commands
|
||||||
--------------------
|
--------------------
|
||||||
@@ -1155,7 +966,7 @@ es10x_store_data
|
|||||||
|
|
||||||
.. argparse::
|
.. argparse::
|
||||||
:module: pySim.euicc
|
:module: pySim.euicc
|
||||||
:func: CardApplicationISDR.AddlShellCommands.es10x_store_data_parser
|
:func: ADF_ISDR.AddlShellCommands.es10x_store_data_parser
|
||||||
|
|
||||||
get_euicc_configured_addresses
|
get_euicc_configured_addresses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -1174,7 +985,7 @@ set_default_dp_address
|
|||||||
|
|
||||||
.. argparse::
|
.. argparse::
|
||||||
:module: pySim.euicc
|
:module: pySim.euicc
|
||||||
:func: CardApplicationISDR.AddlShellCommands.set_def_dp_addr_parser
|
:func: ADF_ISDR.AddlShellCommands.set_def_dp_addr_parser
|
||||||
|
|
||||||
get_euicc_challenge
|
get_euicc_challenge
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -1317,7 +1128,7 @@ remove_notification_from_list
|
|||||||
|
|
||||||
.. argparse::
|
.. argparse::
|
||||||
:module: pySim.euicc
|
:module: pySim.euicc
|
||||||
:func: CardApplicationISDR.AddlShellCommands.rem_notif_parser
|
:func: ADF_ISDR.AddlShellCommands.rem_notif_parser
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
@@ -1366,7 +1177,7 @@ enable_profile
|
|||||||
|
|
||||||
.. argparse::
|
.. argparse::
|
||||||
:module: pySim.euicc
|
:module: pySim.euicc
|
||||||
:func: CardApplicationISDR.AddlShellCommands.en_prof_parser
|
:func: ADF_ISDR.AddlShellCommands.en_prof_parser
|
||||||
|
|
||||||
Example (successful)::
|
Example (successful)::
|
||||||
|
|
||||||
@@ -1388,7 +1199,7 @@ disable_profile
|
|||||||
|
|
||||||
.. argparse::
|
.. argparse::
|
||||||
:module: pySim.euicc
|
:module: pySim.euicc
|
||||||
:func: CardApplicationISDR.AddlShellCommands.dis_prof_parser
|
:func: ADF_ISDR.AddlShellCommands.dis_prof_parser
|
||||||
|
|
||||||
Example (successful)::
|
Example (successful)::
|
||||||
|
|
||||||
@@ -1402,7 +1213,7 @@ delete_profile
|
|||||||
|
|
||||||
.. argparse::
|
.. argparse::
|
||||||
:module: pySim.euicc
|
:module: pySim.euicc
|
||||||
:func: CardApplicationISDR.AddlShellCommands.del_prof_parser
|
:func: ADF_ISDR.AddlShellCommands.del_prof_parser
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
@@ -1411,13 +1222,6 @@ Example::
|
|||||||
"delete_result": "ok"
|
"delete_result": "ok"
|
||||||
}
|
}
|
||||||
|
|
||||||
euicc_memory_reset
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. argparse::
|
|
||||||
:module: pySim.euicc
|
|
||||||
:func: CardApplicationISDR.AddlShellCommands.mem_res_parser
|
|
||||||
|
|
||||||
|
|
||||||
get_eid
|
get_eid
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@@ -1436,7 +1240,7 @@ set_nickname
|
|||||||
|
|
||||||
.. argparse::
|
.. argparse::
|
||||||
:module: pySim.euicc
|
:module: pySim.euicc
|
||||||
:func: CardApplicationISDR.AddlShellCommands.set_nickname_parser
|
:func: ADF_ISDR.AddlShellCommands.set_nickname_parser
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
|
|||||||
@@ -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'}}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,56 +1,40 @@
|
|||||||
|
|
||||||
Guide: Enabling 5G SUCI
|
Guide: Enabling 5G SUCI
|
||||||
=======================
|
========================
|
||||||
|
|
||||||
SUPI/SUCI Concealment is a feature of 5G-Standalone (SA) to encrypt the
|
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
|
IMSI/SUPI with a network operator public key. 3GPP Specifies two different
|
||||||
variants for this:
|
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*
|
* SUCI calculation *on the card itself*
|
||||||
|
|
||||||
pySim supports writing the 5G-specific files for *SUCI calculation in the UE* on USIM cards, assuming
|
pySIM supports writing the 5G-specific files for *SUCI calculation in the UE* on USIM cards, assuming that
|
||||||
that your cards contain the required files, and you have the privileges/credentials to write to them.
|
your cards contain the required files, and you have the privileges/credentials to write to them. This is
|
||||||
This is the case using sysmocom sysmoISIM-SJA2 or any flavor of sysmoISIM-SJA5.
|
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
|
In short, you can enable SUCI with these steps:
|
||||||
supports the vendor-specific method for the sysmoISIM-SJA5-S17).
|
|
||||||
|
|
||||||
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
|
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:
|
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 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): `3GPP TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.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>`__
|
||||||
* Test keys for SUCI calculation: `3GPP TS 33.501 <https://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.pdf>`__
|
|
||||||
|
|
||||||
For specific information on sysmocom SIM cards, refer to
|
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>`__.
|
||||||
* 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
|
|
||||||
|
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
||||||
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
|
Admin PIN
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -99,8 +83,8 @@ By default, the file is present but empty:
|
|||||||
missing Protection Scheme Identifier List data object tag
|
missing Protection Scheme Identifier List data object tag
|
||||||
9000: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -> {}
|
9000: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -> {}
|
||||||
|
|
||||||
The following JSON config defines the testfile from 3GPP TS 31.121, Section 4.9.4 with
|
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 3GPP TS 33.501, Annex C.4. Highest priority (``0``) has a
|
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
|
Profile-B (``identifier: 2``) key in key slot ``1``, which means the key
|
||||||
with ``hnet_pubkey_identifier: 27``.
|
with ``hnet_pubkey_identifier: 27``.
|
||||||
|
|
||||||
@@ -113,7 +97,7 @@ with ``hnet_pubkey_identifier: 27``.
|
|||||||
{"priority": 2, "identifier": 0, "key_index": 0}],
|
{"priority": 2, "identifier": 0, "key_index": 0}],
|
||||||
"hnet_pubkey_list": [
|
"hnet_pubkey_list": [
|
||||||
{"hnet_pubkey_identifier": 27,
|
{"hnet_pubkey_identifier": 27,
|
||||||
"hnet_pubkey": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"},
|
"hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"},
|
||||||
{"hnet_pubkey_identifier": 30,
|
{"hnet_pubkey_identifier": 30,
|
||||||
"hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]
|
"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
|
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.
|
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
|
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]
|
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
|
:widths: 15 40
|
||||||
:header-rows: 1
|
:header-rows: 1
|
||||||
|
|
||||||
@@ -200,7 +184,7 @@ be disabled.
|
|||||||
USIM Error with 5G and sysmoISIM
|
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,
|
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
|
but EF.SUCI_Calc_Info and EF.Routing_Indicator are empty files (hence
|
||||||
do not contain valid data).
|
do not contain valid data).
|
||||||
@@ -209,62 +193,3 @@ At least for Qualcomm’s X55 modem, this results in an USIM error and the
|
|||||||
whole modem shutting 5G down. If you don’t need SUCI concealment but the
|
whole modem shutting 5G down. If you don’t need SUCI concealment but the
|
||||||
smartphone refuses to connect to any 5G network, try to disable the UST
|
smartphone refuses to connect to any 5G network, try to disable the UST
|
||||||
service 124.
|
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
|
|
||||||
|
|||||||
835
osmo-smdpp.py
835
osmo-smdpp.py
File diff suppressed because it is too large
Load Diff
156
pySim-prog.py
156
pySim-prog.py
@@ -25,7 +25,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import argparse
|
from optparse import OptionParser
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
@@ -33,12 +33,11 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
import csv
|
import csv
|
||||||
from osmocom.utils import h2b, swap_nibbles, rpad
|
|
||||||
|
|
||||||
from pySim.commands import SimCardCommands
|
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.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.ts_51_011 import EF_AD
|
||||||
from pySim.legacy.ts_51_011 import EF
|
from pySim.legacy.ts_51_011 import EF
|
||||||
from pySim.card_handler import *
|
from pySim.card_handler import *
|
||||||
@@ -47,146 +46,169 @@ from pySim.utils import *
|
|||||||
|
|
||||||
def parse_options():
|
def parse_options():
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = OptionParser(usage="usage: %prog [options]")
|
||||||
argparse_add_reader_args(parser)
|
|
||||||
|
|
||||||
parser.add_argument("-t", "--type", dest="type",
|
parser.add_option("-d", "--device", dest="device", metavar="DEV",
|
||||||
help="Card type (user -t list to view) [default: %(default)s]",
|
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",
|
default="auto",
|
||||||
)
|
)
|
||||||
parser.add_argument("-T", "--probe", dest="probe",
|
parser.add_option("-T", "--probe", dest="probe",
|
||||||
help="Determine card type",
|
help="Determine card type",
|
||||||
default=False, action="store_true"
|
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)",
|
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",
|
help="ADM PIN used for provisioning, as hex string (16 characters long",
|
||||||
)
|
)
|
||||||
parser.add_argument("-e", "--erase", dest="erase", action='store_true',
|
parser.add_option("-e", "--erase", dest="erase", action='store_true',
|
||||||
help="Erase beforehand [default: %(default)s]",
|
help="Erase beforehand [default: %default]",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("-S", "--source", dest="source",
|
parser.add_option("-S", "--source", dest="source",
|
||||||
help="Data Source[default: %(default)s]",
|
help="Data Source[default: %default]",
|
||||||
default="cmdline",
|
default="cmdline",
|
||||||
)
|
)
|
||||||
|
|
||||||
# if mode is "cmdline"
|
# if mode is "cmdline"
|
||||||
parser.add_argument("-n", "--name", dest="name",
|
parser.add_option("-n", "--name", dest="name",
|
||||||
help="Operator name [default: %(default)s]",
|
help="Operator name [default: %default]",
|
||||||
default="Magic",
|
default="Magic",
|
||||||
)
|
)
|
||||||
parser.add_argument("-c", "--country", dest="country", type=int, metavar="CC",
|
parser.add_option("-c", "--country", dest="country", type="int", metavar="CC",
|
||||||
help="Country code [default: %(default)s]",
|
help="Country code [default: %default]",
|
||||||
default=1,
|
default=1,
|
||||||
)
|
)
|
||||||
parser.add_argument("-x", "--mcc", dest="mcc",
|
parser.add_option("-x", "--mcc", dest="mcc", type="string",
|
||||||
help="Mobile Country Code [default: %(default)s]",
|
help="Mobile Country Code [default: %default]",
|
||||||
default="901",
|
default="901",
|
||||||
)
|
)
|
||||||
parser.add_argument("-y", "--mnc", dest="mnc",
|
parser.add_option("-y", "--mnc", dest="mnc", type="string",
|
||||||
help="Mobile Network Code [default: %(default)s]",
|
help="Mobile Network Code [default: %default]",
|
||||||
default="55",
|
default="55",
|
||||||
)
|
)
|
||||||
parser.add_argument("--mnclen", dest="mnclen",
|
parser.add_option("--mnclen", dest="mnclen", type="choice",
|
||||||
help="Length of Mobile Network Code [default: %(default)s]",
|
help="Length of Mobile Network Code [default: %default]",
|
||||||
default="auto",
|
default="auto",
|
||||||
choices=["2", "3", "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']",
|
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]",
|
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",
|
help="Integrated Circuit Card ID",
|
||||||
)
|
)
|
||||||
parser.add_argument("-i", "--imsi", dest="imsi",
|
parser.add_option("-i", "--imsi", dest="imsi",
|
||||||
help="International Mobile Subscriber Identity",
|
help="International Mobile Subscriber Identity",
|
||||||
)
|
)
|
||||||
parser.add_argument("--msisdn", dest="msisdn",
|
parser.add_option("--msisdn", dest="msisdn",
|
||||||
help="Mobile Subscriber Integrated Services Digital Number",
|
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)",
|
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)",
|
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",
|
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",
|
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)",
|
help="Set UE Operation Mode in EF.AD (Administrative Data)",
|
||||||
default=None,
|
default=None,
|
||||||
choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
|
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",
|
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)",
|
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)",
|
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)",
|
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",
|
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",
|
help="Set IMS private user identity",
|
||||||
)
|
)
|
||||||
parser.add_argument("--impu", dest="impu",
|
parser.add_option("--impu", dest="impu",
|
||||||
help="Set IMS public user identity",
|
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
|
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
|
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",
|
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",
|
help="Card # used for ICCID/IMSI autogen",
|
||||||
)
|
)
|
||||||
parser.add_argument("--batch", dest="batch_mode",
|
parser.add_option("--batch", dest="batch_mode",
|
||||||
help="Enable batch mode [default: %(default)s]",
|
help="Enable batch mode [default: %default]",
|
||||||
default=False, action='store_true',
|
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",
|
help="Optional batch state file",
|
||||||
)
|
)
|
||||||
|
|
||||||
# if mode is "csv"
|
# 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")
|
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",
|
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",
|
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",
|
help="Perform a 'dry run', don't actually program the card",
|
||||||
default=False, action="store_true")
|
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")
|
help="Use automatic card handling machine")
|
||||||
|
|
||||||
options = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
if options.type == 'list':
|
if options.type == 'list':
|
||||||
for kls in _cards_classes:
|
for kls in _cards_classes:
|
||||||
@@ -197,13 +219,15 @@ def parse_options():
|
|||||||
return options
|
return options
|
||||||
|
|
||||||
if options.source == 'csv':
|
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):
|
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 requires one additional parameter: --read-iccid, --read-imsi, --iccid or --imsi")
|
parser.error(
|
||||||
|
"CSV mode needs either an IMSI, --read-imsi, --read-iccid or batch mode")
|
||||||
if options.read_csv is None:
|
if options.read_csv is None:
|
||||||
parser.error("CSV mode requires a CSV input file")
|
parser.error("CSV mode requires a CSV input file")
|
||||||
elif options.source == 'cmdline':
|
elif options.source == 'cmdline':
|
||||||
if ((options.imsi is None) or (options.iccid is None)) and (options.num is None):
|
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:
|
else:
|
||||||
parser.error("Only `cmdline' and `csv' sources supported")
|
parser.error("Only `cmdline' and `csv' sources supported")
|
||||||
|
|
||||||
@@ -218,6 +242,9 @@ def parse_options():
|
|||||||
parser.error(
|
parser.error(
|
||||||
"Can't give ICCID/IMSI for batch mode, need to use automatic parameters ! see --num and --secret for more information")
|
"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
|
return options
|
||||||
|
|
||||||
|
|
||||||
@@ -586,7 +613,7 @@ def read_params_csv(opts, imsi=None, iccid=None):
|
|||||||
else:
|
else:
|
||||||
row['mnc'] = row.get('mnc', mnc_from_imsi(row.get('imsi'), False))
|
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
|
# CSV files for a better automatization. However, this only makes sense
|
||||||
# when the tools and databases we export our files from will also add
|
# when the tools and databases we export our files from will also add
|
||||||
# such a field.
|
# such a field.
|
||||||
@@ -622,9 +649,6 @@ def read_params_csv(opts, imsi=None, iccid=None):
|
|||||||
|
|
||||||
def write_params_hlr(opts, params):
|
def write_params_hlr(opts, params):
|
||||||
# SQLite3 OpenBSC HLR
|
# 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:
|
if opts.write_hlr:
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect(opts.write_hlr)
|
conn = sqlite3.connect(opts.write_hlr)
|
||||||
@@ -725,18 +749,16 @@ def process_card(scc, opts, first, ch):
|
|||||||
card.erase()
|
card.erase()
|
||||||
card.reset()
|
card.reset()
|
||||||
|
|
||||||
cp = None
|
|
||||||
|
|
||||||
# Generate parameters
|
# Generate parameters
|
||||||
if opts.source == 'cmdline':
|
if opts.source == 'cmdline':
|
||||||
cp = gen_parameters(opts)
|
cp = gen_parameters(opts)
|
||||||
elif opts.source == 'csv':
|
elif opts.source == 'csv':
|
||||||
|
imsi = None
|
||||||
|
iccid = None
|
||||||
if opts.read_iccid:
|
if opts.read_iccid:
|
||||||
(res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
|
(res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
|
||||||
iccid = dec_iccid(res)
|
iccid = dec_iccid(res)
|
||||||
else:
|
elif opts.read_imsi:
|
||||||
iccid = opts.iccid
|
|
||||||
if opts.read_imsi:
|
|
||||||
(res, _) = scc.read_binary(EF['IMSI'])
|
(res, _) = scc.read_binary(EF['IMSI'])
|
||||||
imsi = swap_nibbles(res)[3:]
|
imsi = swap_nibbles(res)[3:]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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>
|
# Copyright (C) 2009 Sylvain Munaut <tnt@246tNt.com>
|
||||||
@@ -29,8 +29,6 @@ import random
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from osmocom.utils import h2b, h2s, swap_nibbles, rpad
|
|
||||||
|
|
||||||
from pySim.ts_51_011 import EF_SST_map, EF_AD
|
from pySim.ts_51_011 import EF_SST_map, EF_AD
|
||||||
from pySim.legacy.ts_51_011 import EF, DF
|
from pySim.legacy.ts_51_011 import EF, DF
|
||||||
from pySim.ts_31_102 import EF_UST_map
|
from pySim.ts_31_102 import EF_UST_map
|
||||||
@@ -42,8 +40,8 @@ from pySim.commands import SimCardCommands
|
|||||||
from pySim.transport import init_reader, argparse_add_reader_args
|
from pySim.transport import init_reader, argparse_add_reader_args
|
||||||
from pySim.exceptions import SwMatchError
|
from pySim.exceptions import SwMatchError
|
||||||
from pySim.legacy.cards import card_detect, SimCard, UsimCard, IsimCard
|
from pySim.legacy.cards import card_detect, SimCard, UsimCard, IsimCard
|
||||||
from pySim.utils import dec_imsi, dec_iccid
|
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, 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',
|
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
@@ -88,7 +86,7 @@ if __name__ == '__main__':
|
|||||||
scc.sel_ctrl = "0004"
|
scc.sel_ctrl = "0004"
|
||||||
|
|
||||||
# Testing for Classic SIM or UICC
|
# 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':
|
if sw == '6e00':
|
||||||
# Just a Classic SIM
|
# Just a Classic SIM
|
||||||
scc.cla_byte = "a0"
|
scc.cla_byte = "a0"
|
||||||
|
|||||||
755
pySim-shell.py
755
pySim-shell.py
File diff suppressed because it is too large
Load Diff
@@ -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()
|
|
||||||
|
|
||||||
@@ -8,21 +8,17 @@ from pprint import pprint as pp
|
|||||||
from pySim.apdu import *
|
from pySim.apdu import *
|
||||||
from pySim.runtime import RuntimeState
|
from pySim.runtime import RuntimeState
|
||||||
|
|
||||||
from osmocom.utils import JsonEncoder
|
|
||||||
|
|
||||||
from pySim.cards import UiccCardBase
|
from pySim.cards import UiccCardBase
|
||||||
from pySim.commands import SimCardCommands
|
from pySim.commands import SimCardCommands
|
||||||
from pySim.profile import CardProfile
|
from pySim.profile import CardProfile
|
||||||
from pySim.ts_102_221 import CardProfileUICC
|
from pySim.ts_102_221 import CardProfileUICC
|
||||||
from pySim.ts_31_102 import CardApplicationUSIM
|
from pySim.ts_31_102 import CardApplicationUSIM
|
||||||
from pySim.ts_31_103 import CardApplicationISIM
|
from pySim.ts_31_103 import CardApplicationISIM
|
||||||
from pySim.euicc import CardApplicationISDR, CardApplicationECASD
|
|
||||||
from pySim.transport import LinkBase
|
from pySim.transport import LinkBase
|
||||||
|
|
||||||
from pySim.apdu_source.gsmtap import GsmtapApduSource
|
from pySim.apdu_source.gsmtap import GsmtapApduSource
|
||||||
from pySim.apdu_source.pyshark_rspro import PysharkRsproPcap, PysharkRsproLive
|
from pySim.apdu_source.pyshark_rspro import PysharkRsproPcap, PysharkRsproLive
|
||||||
from pySim.apdu_source.pyshark_gsmtap import PysharkGsmtapPcap
|
from pySim.apdu_source.pyshark_gsmtap import PysharkGsmtapPcap
|
||||||
from pySim.apdu_source.tca_loader_log import TcaLoaderLogApduSource
|
|
||||||
|
|
||||||
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
|
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
|
||||||
|
|
||||||
@@ -55,7 +51,7 @@ class DummySimLink(LinkBase):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "dummy"
|
return "dummy"
|
||||||
|
|
||||||
def _send_apdu(self, pdu):
|
def _send_apdu_raw(self, pdu):
|
||||||
#print("DummySimLink-apdu: %s" % pdu)
|
#print("DummySimLink-apdu: %s" % pdu)
|
||||||
return [], '9000'
|
return [], '9000'
|
||||||
|
|
||||||
@@ -65,7 +61,7 @@ class DummySimLink(LinkBase):
|
|||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _reset_card(self):
|
def reset_card(self):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def get_atr(self):
|
def get_atr(self):
|
||||||
@@ -82,8 +78,6 @@ class Tracer:
|
|||||||
profile = CardProfileUICC()
|
profile = CardProfileUICC()
|
||||||
profile.add_application(CardApplicationUSIM())
|
profile.add_application(CardApplicationUSIM())
|
||||||
profile.add_application(CardApplicationISIM())
|
profile.add_application(CardApplicationISIM())
|
||||||
profile.add_application(CardApplicationISDR())
|
|
||||||
profile.add_application(CardApplicationECASD())
|
|
||||||
scc = SimCardCommands(transport=DummySimLink())
|
scc = SimCardCommands(transport=DummySimLink())
|
||||||
card = UiccCardBase(scc)
|
card = UiccCardBase(scc)
|
||||||
self.rs = RuntimeState(card, profile)
|
self.rs = RuntimeState(card, profile)
|
||||||
@@ -99,8 +93,7 @@ class Tracer:
|
|||||||
"""Output a single decoded + processed ApduCommand."""
|
"""Output a single decoded + processed ApduCommand."""
|
||||||
if self.show_raw_apdu:
|
if self.show_raw_apdu:
|
||||||
print(apdu)
|
print(apdu)
|
||||||
print("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id,
|
print("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id, inst.col_sw, inst.processed))
|
||||||
inst.col_sw, json.dumps(inst.processed, cls=JsonEncoder)))
|
|
||||||
print("===============================")
|
print("===============================")
|
||||||
|
|
||||||
def format_reset(self, apdu: CardReset):
|
def format_reset(self, apdu: CardReset):
|
||||||
@@ -151,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',
|
global_group.add_argument('--no-suppress-status', action='store_false', dest='suppress_status',
|
||||||
help="""
|
help="""
|
||||||
Don't suppress displaying STATUS APDUs. We normally suppress them as they don't provide any
|
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',
|
global_group.add_argument('--show-raw-apdu', action='store_true', dest='show_raw_apdu',
|
||||||
help="""Show the raw APDU in addition to its parsed form.""")
|
help="""Show the raw APDU in addition to its parsed form.""")
|
||||||
|
|
||||||
@@ -185,11 +178,6 @@ parser_rspro_pyshark_live = subparsers.add_parser('rspro-pyshark-live', help="""
|
|||||||
parser_rspro_pyshark_live.add_argument('-i', '--interface', required=True,
|
parser_rspro_pyshark_live.add_argument('-i', '--interface', required=True,
|
||||||
help='Name of the network interface to capture on')
|
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')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
opts = option_parser.parse_args()
|
opts = option_parser.parse_args()
|
||||||
@@ -203,10 +191,6 @@ if __name__ == '__main__':
|
|||||||
s = PysharkRsproLive(opts.interface)
|
s = PysharkRsproLive(opts.interface)
|
||||||
elif opts.source == 'gsmtap-pyshark-pcap':
|
elif opts.source == 'gsmtap-pyshark-pcap':
|
||||||
s = PysharkGsmtapPcap(opts.pcap_file)
|
s = PysharkGsmtapPcap(opts.pcap_file)
|
||||||
elif opts.source == 'tca-loader-log':
|
|
||||||
s = TcaLoaderLogApduSource(opts.log_file)
|
|
||||||
else:
|
|
||||||
raise ValueError("unsupported source %s", opts.source)
|
|
||||||
|
|
||||||
tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select,
|
tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select,
|
||||||
show_raw_apdu=opts.show_raw_apdu)
|
show_raw_apdu=opts.show_raw_apdu)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# coding=utf-8
|
||||||
"""APDU (and TPDU) parser for UICC/USIM/ISIM cards.
|
"""APDU (and TPDU) parser for UICC/USIM/ISIM cards.
|
||||||
|
|
||||||
The File (and its classes) represent the structure / hierarchy
|
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.
|
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
|
# 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
|
# 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
|
import abc
|
||||||
|
from termcolor import colored
|
||||||
import typing
|
import typing
|
||||||
from typing import List, Dict, Optional
|
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.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
|
||||||
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
|
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
|
||||||
|
|
||||||
@@ -51,8 +52,8 @@ from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
|
|||||||
class ApduCommandMeta(abc.ABCMeta):
|
class ApduCommandMeta(abc.ABCMeta):
|
||||||
"""A meta-class that we can use to set some class variables when declaring
|
"""A meta-class that we can use to set some class variables when declaring
|
||||||
a derived class of ApduCommand."""
|
a derived class of ApduCommand."""
|
||||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
def __new__(metacls, name, bases, namespace, **kwargs):
|
||||||
x = super().__new__(mcs, name, bases, namespace)
|
x = super().__new__(metacls, name, bases, namespace)
|
||||||
x._name = namespace.get('name', kwargs.get('n', None))
|
x._name = namespace.get('name', kwargs.get('n', None))
|
||||||
x._ins = namespace.get('ins', kwargs.get('ins', None))
|
x._ins = namespace.get('ins', kwargs.get('ins', None))
|
||||||
x._cla = namespace.get('cla', kwargs.get('cla', 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
|
# fall-back constructs if the derived class provides no override
|
||||||
_construct_p1 = Byte
|
_construct_p1 = Byte
|
||||||
_construct_p2 = Byte
|
_construct_p2 = Byte
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
_construct_rsp = GreedyBytes
|
_construct_rsp = HexAdapter(GreedyBytes)
|
||||||
_tlv = None
|
|
||||||
_tlv_rsp = None
|
|
||||||
|
|
||||||
def __init__(self, cmd: BytesOrHex, rsp: Optional[BytesOrHex] = None):
|
def __init__(self, cmd: BytesOrHex, rsp: Optional[BytesOrHex] = None):
|
||||||
"""Instantiate a new ApduCommand from give cmd + resp."""
|
"""Instantiate a new ApduCommand from give cmd + resp."""
|
||||||
@@ -188,39 +187,44 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
|||||||
if apdu_case in [1, 2]:
|
if apdu_case in [1, 2]:
|
||||||
# data is part of response
|
# data is part of response
|
||||||
return cls(buffer[:5], buffer[5:])
|
return cls(buffer[:5], buffer[5:])
|
||||||
if apdu_case in [3, 4]:
|
elif apdu_case in [3, 4]:
|
||||||
# data is part of command
|
# data is part of command
|
||||||
lc = buffer[4]
|
lc = buffer[4]
|
||||||
return cls(buffer[:5+lc], buffer[5+lc:])
|
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
|
@property
|
||||||
def path(self) -> List[str]:
|
def path(self) -> List[str]:
|
||||||
"""Return (if known) the path as list of files to the file on which this command operates."""
|
"""Return (if known) the path as list of files to the file on which this command operates."""
|
||||||
if self.file:
|
if self.file:
|
||||||
return self.file.fully_qualified_path()
|
return self.file.fully_qualified_path()
|
||||||
return []
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_str(self) -> str:
|
def path_str(self) -> str:
|
||||||
"""Return (if known) the path as string to the file on which this command operates."""
|
"""Return (if known) the path as string to the file on which this command operates."""
|
||||||
if self.file:
|
if self.file:
|
||||||
return self.file.fully_qualified_path_str()
|
return self.file.fully_qualified_path_str()
|
||||||
return ''
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def col_sw(self) -> str:
|
def col_sw(self) -> str:
|
||||||
"""Return the ansi-colorized status word. Green==OK, Red==Error"""
|
"""Return the ansi-colorized status word. Green==OK, Red==Error"""
|
||||||
if self.successful:
|
if self.successful:
|
||||||
return colored(b2h(self.sw), 'green')
|
return colored(b2h(self.sw), 'green')
|
||||||
return colored(b2h(self.sw), 'red')
|
else:
|
||||||
|
return colored(b2h(self.sw), 'red')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lchan_nr(self) -> int:
|
def lchan_nr(self) -> int:
|
||||||
"""Logical channel number over which this ApduCommand was transmitted."""
|
"""Logical channel number over which this ApduCommand was transmitted."""
|
||||||
if self.lchan:
|
if self.lchan:
|
||||||
return self.lchan.lchan_nr
|
return self.lchan.lchan_nr
|
||||||
return lchan_nr_from_cla(self.cla)
|
else:
|
||||||
|
return lchan_nr_from_cla(self.cla)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '%02u %s(%s): %s' % (self.lchan_nr, type(self).__name__, self.path_str, self.to_dict())
|
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
|
"""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."""
|
process_global or process_on_lchan method. Uses information from APDU decode."""
|
||||||
self.processed = {}
|
self.processed = {}
|
||||||
if 'p1' not in self.cmd_dict:
|
if not 'p1' in self.cmd_dict:
|
||||||
self.processed = self.to_dict()
|
self.processed = self.to_dict()
|
||||||
else:
|
else:
|
||||||
self.processed['p1'] = self.cmd_dict['p1']
|
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?."""
|
"""Does the given CLA match the CLA list of the command?."""
|
||||||
if not isinstance(cla, str):
|
if not isinstance(cla, str):
|
||||||
cla = '%02X' % cla
|
cla = '%02X' % cla
|
||||||
cla = cla.upper()
|
cla = cla.lower()
|
||||||
# see https://github.com/PyCQA/pylint/issues/7219
|
# see https://github.com/PyCQA/pylint/issues/7219
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
for cla_match in cls._cla:
|
for cla_match in cls._cla:
|
||||||
@@ -281,7 +285,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
|||||||
cla_masked += 'X'
|
cla_masked += 'X'
|
||||||
else:
|
else:
|
||||||
cla_masked += cla[i]
|
cla_masked += cla[i]
|
||||||
if cla_masked == cla_match.upper():
|
if cla_masked == cla_match:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -291,26 +295,17 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
|||||||
if callable(method):
|
if callable(method):
|
||||||
return method()
|
return method()
|
||||||
else:
|
else:
|
||||||
return self._cmd_to_dict()
|
r = {}
|
||||||
|
method = getattr(self, '_decode_p1p2', None)
|
||||||
def _cmd_to_dict(self) -> Dict:
|
if callable(method):
|
||||||
"""back-end function performing automatic decoding using _construct / _tlv."""
|
r = self._decode_p1p2()
|
||||||
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()
|
|
||||||
else:
|
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)
|
r['body'] = parse_construct(self._construct, self.cmd_data)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def rsp_to_dict(self) -> Dict:
|
def rsp_to_dict(self) -> Dict:
|
||||||
"""Convert the Response part of the APDU to a dict."""
|
"""Convert the Response part of the APDU to a dict."""
|
||||||
@@ -320,12 +315,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
|||||||
else:
|
else:
|
||||||
r = {}
|
r = {}
|
||||||
if self.rsp_data:
|
if self.rsp_data:
|
||||||
if self._tlv_rsp:
|
r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
|
||||||
ie = self._tlv_rsp()
|
|
||||||
ie.from_tlv(self.rsp_data)
|
|
||||||
r['body'] = ie.to_dict()
|
|
||||||
else:
|
|
||||||
r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
|
|
||||||
r['sw'] = b2h(self.sw)
|
r['sw'] = b2h(self.sw)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@@ -438,7 +428,8 @@ class TpduFilter(ApduHandler):
|
|||||||
apdu = Apdu(icmd, tpdu.rsp)
|
apdu = Apdu(icmd, tpdu.rsp)
|
||||||
if self.apdu_handler:
|
if self.apdu_handler:
|
||||||
return self.apdu_handler.input(apdu)
|
return self.apdu_handler.input(apdu)
|
||||||
return Apdu(icmd, tpdu.rsp)
|
else:
|
||||||
|
return Apdu(icmd, tpdu.rsp)
|
||||||
|
|
||||||
def input(self, cmd: bytes, rsp: bytes):
|
def input(self, cmd: bytes, rsp: bytes):
|
||||||
if isinstance(cmd, str):
|
if isinstance(cmd, str):
|
||||||
@@ -461,6 +452,7 @@ class CardReset:
|
|||||||
self.atr = atr
|
self.atr = atr
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.atr:
|
if (self.atr):
|
||||||
return '%s(%s)' % (type(self).__name__, b2h(self.atr))
|
return '%s(%s)' % (type(self).__name__, b2h(self.atr))
|
||||||
return '%s' % (type(self).__name__)
|
else:
|
||||||
|
return '%s' % (type(self).__name__)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
"""APDU definition/decoder of GlobalPLatform Card Spec (currently 2.1.1)
|
"""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
|
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
|
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/>.
|
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.apdu import ApduCommand, ApduCommandSet
|
||||||
from pySim.global_platform import InstallParameters
|
|
||||||
|
|
||||||
class GpDelete(ApduCommand, n='DELETE', ins=0xE4, cla=['8X', 'CX', 'EX']):
|
class GpDelete(ApduCommand, n='DELETE', ins=0xE4, cla=['8X', 'CX', 'EX']):
|
||||||
_apdu_case = 4
|
_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']):
|
class GpGetStatus(ApduCommand, n='GET STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
|
||||||
_apdu_case = 4
|
_apdu_case = 4
|
||||||
|
|
||||||
# GPCS Section 11.5.2
|
|
||||||
class GpInstall(ApduCommand, n='INSTALL', ins=0xE6, cla=['8X', 'CX', 'EX']):
|
class GpInstall(ApduCommand, n='INSTALL', ins=0xE6, cla=['8X', 'CX', 'EX']):
|
||||||
_apdu_case = 4
|
_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']):
|
class GpLoad(ApduCommand, n='LOAD', ins=0xE8, cla=['8X', 'CX', 'EX']):
|
||||||
_apdu_case = 4
|
_apdu_case = 4
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
"""APDU definitions/decoders of ETSI TS 102 221, the core UICC spec.
|
"""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
|
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
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional, Dict
|
|
||||||
import logging
|
import logging
|
||||||
|
from pySim.construct import *
|
||||||
from construct import GreedyRange, Struct
|
|
||||||
|
|
||||||
from osmocom.utils import i2h
|
|
||||||
from osmocom.construct import *
|
|
||||||
|
|
||||||
from pySim.filesystem import *
|
from pySim.filesystem import *
|
||||||
from pySim.runtime import RuntimeLchan
|
from pySim.runtime import RuntimeLchan
|
||||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||||
from pySim import cat
|
from typing import Optional, Dict, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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)
|
logger.warning('SELECT UNKNOWN FID %s', file_hex)
|
||||||
elif mode == 'df_name':
|
elif mode == 'df_name':
|
||||||
# Select by AID (can be sub-string!)
|
# 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'])
|
sels = lchan.rs.mf.get_app_selectables(['AIDS'])
|
||||||
adf = self._find_aid_substr(sels, aid)
|
adf = self._find_aid_substr(sels, aid)
|
||||||
if adf:
|
if adf:
|
||||||
@@ -109,6 +103,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
|
|||||||
#print("\tSELECT AID %s" % adf)
|
#print("\tSELECT AID %s" % adf)
|
||||||
else:
|
else:
|
||||||
logger.warning('SELECT UNKNOWN AID %s', aid)
|
logger.warning('SELECT UNKNOWN AID %s', aid)
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValueError('Select Mode %s not implemented' % mode)
|
raise ValueError('Select Mode %s not implemented' % mode)
|
||||||
# decode the SELECT response
|
# decode the SELECT response
|
||||||
@@ -116,7 +111,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
|
|||||||
self.file = lchan.selected_file
|
self.file = lchan.selected_file
|
||||||
if 'body' in self.rsp_dict:
|
if 'body' in self.rsp_dict:
|
||||||
# not every SELECT is asking for the FCP in response...
|
# 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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -129,7 +124,7 @@ class UiccStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
|
|||||||
|
|
||||||
def process_on_lchan(self, lchan):
|
def process_on_lchan(self, lchan):
|
||||||
if self.cmd_dict['p2'] == 'response_like_select':
|
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:
|
def _decode_binary_p1p2(p1, p2) -> Dict:
|
||||||
ret = {}
|
ret = {}
|
||||||
@@ -295,9 +290,12 @@ class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _pin_is_success(sw):
|
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)
|
return VerifyPin._pin_process(self)
|
||||||
|
|
||||||
def _is_success(self):
|
def _is_success(self):
|
||||||
@@ -309,7 +307,7 @@ class ChangePin(ApduCommand, n='CHANGE PIN', ins=0x24, cla=['0X', '4X', '6X']):
|
|||||||
_apdu_case = 3
|
_apdu_case = 3
|
||||||
_construct_p2 = PinConstructP2
|
_construct_p2 = PinConstructP2
|
||||||
|
|
||||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||||
return VerifyPin._pin_process(self)
|
return VerifyPin._pin_process(self)
|
||||||
|
|
||||||
def _is_success(self):
|
def _is_success(self):
|
||||||
@@ -321,7 +319,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
|
|||||||
_apdu_case = 3
|
_apdu_case = 3
|
||||||
_construct_p2 = PinConstructP2
|
_construct_p2 = PinConstructP2
|
||||||
|
|
||||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||||
return VerifyPin._pin_process(self)
|
return VerifyPin._pin_process(self)
|
||||||
|
|
||||||
def _is_success(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']):
|
class EnablePin(ApduCommand, n='ENABLE PIN', ins=0x28, cla=['0X', '4X', '6X']):
|
||||||
_apdu_case = 3
|
_apdu_case = 3
|
||||||
_construct_p2 = PinConstructP2
|
_construct_p2 = PinConstructP2
|
||||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||||
return VerifyPin._pin_process(self)
|
return VerifyPin._pin_process(self)
|
||||||
|
|
||||||
def _is_success(self):
|
def _is_success(self):
|
||||||
@@ -344,7 +342,7 @@ class UnblockPin(ApduCommand, n='UNBLOCK PIN', ins=0x2C, cla=['0X', '4X', '6X'])
|
|||||||
_apdu_case = 3
|
_apdu_case = 3
|
||||||
_construct_p2 = PinConstructP2
|
_construct_p2 = PinConstructP2
|
||||||
|
|
||||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||||
return VerifyPin._pin_process(self)
|
return VerifyPin._pin_process(self)
|
||||||
|
|
||||||
def _is_success(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)
|
manage_channel.add_lchan(created_channel_nr)
|
||||||
self.col_id = '%02u' % created_channel_nr
|
self.col_id = '%02u' % created_channel_nr
|
||||||
return {'mode': mode, 'created_channel': 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']
|
closed_channel_nr = self.cmd_dict['p2']['logical_channel_number']
|
||||||
rs.del_lchan(closed_channel_nr)
|
rs.del_lchan(closed_channel_nr)
|
||||||
self.col_id = '%02u' % closed_channel_nr
|
self.col_id = '%02u' % closed_channel_nr
|
||||||
return {'mode': mode, 'closed_channel': 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
|
# TS 102 221 Section 11.1.18
|
||||||
class GetChallenge(ApduCommand, n='GET CHALLENGE', ins=0x84, cla=['0X', '4X', '6X']):
|
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]
|
p2 = hdr[3]
|
||||||
if p1 & 0x7 == 0: # retrieve UICC Endpoints
|
if p1 & 0x7 == 0: # retrieve UICC Endpoints
|
||||||
return 2
|
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
|
p2_cmd = p2 >> 5
|
||||||
if p2_cmd in [0,2,4]: # command data
|
if p2_cmd in [0,2,4]: # command data
|
||||||
return 3
|
return 3
|
||||||
if p2_cmd in [1,3,5]: # response data
|
elif p2_cmd in [1,3,5]: # response data
|
||||||
return 2
|
return 2
|
||||||
if p1 & 0xf == 4: # terminate secure channel SA
|
elif p1 & 0xf == 4: # terminate secure channel SA
|
||||||
return 3
|
return 3
|
||||||
raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
|
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]
|
p1 = hdr[2]
|
||||||
if p1 & 0x04:
|
if p1 & 0x04:
|
||||||
return 3
|
return 3
|
||||||
return 2
|
else:
|
||||||
|
return 2
|
||||||
|
|
||||||
# TS 102 221 Section 11.1.22
|
# TS 102 221 Section 11.1.22
|
||||||
class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
|
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
|
# TS 102 221 Section 11.2.2 / TS 102 223
|
||||||
class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
|
class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
|
||||||
_apdu_case = 4
|
_apdu_case = 4
|
||||||
_tlv = cat.EventCollection
|
|
||||||
|
|
||||||
# TS 102 221 Section 11.2.3 / TS 102 223
|
# TS 102 221 Section 11.2.3 / TS 102 223
|
||||||
class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
|
class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
|
||||||
_apdu_case = 2
|
_apdu_case = 2
|
||||||
_tlv_rsp = cat.ProactiveCommand
|
|
||||||
|
|
||||||
# TS 102 221 Section 11.2.3 / TS 102 223
|
# TS 102 221 Section 11.2.3 / TS 102 223
|
||||||
class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
|
class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
|
||||||
_apdu_case = 3
|
_apdu_case = 3
|
||||||
_tlv = cat.TerminalResponse
|
|
||||||
|
|
||||||
# TS 102 221 Section 11.3.1
|
# TS 102 221 Section 11.3.1
|
||||||
class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
|
class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
|
||||||
|
|||||||
@@ -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])
|
|
||||||
@@ -9,12 +9,12 @@ APDU commands of 3GPP TS 31.102 V16.6.0
|
|||||||
"""
|
"""
|
||||||
from typing import Dict
|
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 construct import Optional as COptional
|
||||||
from osmocom.construct import *
|
|
||||||
|
|
||||||
from pySim.filesystem import *
|
from pySim.filesystem import *
|
||||||
|
from pySim.construct import *
|
||||||
from pySim.ts_31_102 import SUCI_TlvDataObject
|
from pySim.ts_31_102 import SUCI_TlvDataObject
|
||||||
|
|
||||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||||
|
|
||||||
# Copyright (C) 2022 Harald Welte <laforge@osmocom.org>
|
# 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
|
# Mapping between USIM Service Number and its description
|
||||||
|
|
||||||
|
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||||
|
|
||||||
# TS 31.102 Section 7.1
|
# TS 31.102 Section 7.1
|
||||||
class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '4X', '6X']):
|
class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '4X', '6X']):
|
||||||
_apdu_case = 4
|
_apdu_case = 4
|
||||||
@@ -42,28 +44,28 @@ class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '
|
|||||||
BitsInteger(4),
|
BitsInteger(4),
|
||||||
'authentication_context'/Enum(BitsInteger(3), gsm=0, umts=1,
|
'authentication_context'/Enum(BitsInteger(3), gsm=0, umts=1,
|
||||||
vgcs_vbs=2, gba=4))
|
vgcs_vbs=2, gba=4))
|
||||||
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
|
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
|
||||||
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, Bytes(this._autn_len)))
|
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, HexAdapter(Bytes(this._autn_len))))
|
||||||
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/Bytes(this._vsid_len),
|
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/HexAdapter(Bytes(this._vsid_len)),
|
||||||
'_vkid_len'/Int8ub, 'vk_id'/Bytes(this._vkid_len),
|
'_vkid_len'/Int8ub, 'vk_id'/HexAdapter(Bytes(this._vkid_len)),
|
||||||
'_vstk_rand_len'/Int8ub, 'vstk_rand'/Bytes(this._vstk_rand_len))
|
'_vstk_rand_len'/Int8ub, 'vstk_rand'/HexAdapter(Bytes(this._vstk_rand_len)))
|
||||||
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
|
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
|
||||||
'_autn_len'/Int8ub, 'autn'/Bytes(this._autn_len))
|
'_autn_len'/Int8ub, 'autn'/HexAdapter(Bytes(this._autn_len)))
|
||||||
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/Bytes(this._naf_id_len),
|
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/HexAdapter(Bytes(this._naf_id_len)),
|
||||||
'_impi_len'/Int8ub, 'impi'/Bytes(this._impi_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,
|
_cs_cmd_gba = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDD: 'bootstrap'/_cmd_gba_bs,
|
||||||
0xDE: 'naf_derivation'/_cmd_gba_naf }))
|
0xDE: 'naf_derivation'/_cmd_gba_naf }))
|
||||||
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/Bytes(this._len_sres),
|
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/HexAdapter(Bytes(this._len_sres)),
|
||||||
'_len_kc'/Int8ub, 'kc'/Bytes(this._len_kc))
|
'_len_kc'/Int8ub, 'kc'/HexAdapter(Bytes(this._len_kc)))
|
||||||
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/Bytes(this._len_res),
|
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/HexAdapter(Bytes(this._len_res)),
|
||||||
'_len_ck'/Int8ub, 'ck'/Bytes(this._len_ck),
|
'_len_ck'/Int8ub, 'ck'/HexAdapter(Bytes(this._len_ck)),
|
||||||
'_len_ik'/Int8ub, 'ik'/Bytes(this._len_ik),
|
'_len_ik'/Int8ub, 'ik'/HexAdapter(Bytes(this._len_ik)),
|
||||||
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, Bytes(this._len_kc)))
|
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, HexAdapter(Bytes(this._len_kc))))
|
||||||
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/Bytes(this._len_auts))
|
_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,
|
_cs_rsp_3g = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDB: 'success'/_rsp_3g_ok,
|
||||||
0xDC: 'sync_fail'/_rsp_3g_sync}))
|
0xDC: 'sync_fail'/_rsp_3g_sync}))
|
||||||
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/Bytes(this._vstk_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'/Bytes(this._ks_ext_naf_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:
|
def _decode_cmd(self) -> Dict:
|
||||||
r = {}
|
r = {}
|
||||||
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
|
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ApduSource(abc.ABC):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def read_packet(self) -> PacketType:
|
def read_packet(self) -> PacketType:
|
||||||
"""Read one packet from the source."""
|
"""Read one packet from the source."""
|
||||||
|
pass
|
||||||
|
|
||||||
def read(self) -> Union[Apdu, CardReset]:
|
def read(self) -> Union[Apdu, CardReset]:
|
||||||
"""Main function to call by the user: Blocking read, returns Apdu or 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):
|
elif isinstance(r, CardReset):
|
||||||
apdu = r
|
apdu = r
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown read_packet() return %s' % r)
|
ValueError('Unknown read_packet() return %s' % r)
|
||||||
return apdu
|
return apdu
|
||||||
|
|||||||
@@ -16,16 +16,13 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from osmocom.gsmtap import GsmtapReceiver
|
from pySim.gsmtap import GsmtapMessage, GsmtapSource
|
||||||
|
|
||||||
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
|
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):
|
class GsmtapApduSource(ApduSource):
|
||||||
"""ApduSource for handling GSMTAP-SIM messages received via UDP, such as
|
"""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)
|
bind_port: UDP port number to which the socket should be bound (default: 4729)
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.gsmtap = GsmtapReceiver(bind_ip, bind_port)
|
self.gsmtap = GsmtapSource(bind_ip, bind_port)
|
||||||
|
|
||||||
def read_packet(self) -> PacketType:
|
def read_packet(self) -> PacketType:
|
||||||
gsmtap_msg, _addr = self.gsmtap.read_packet()
|
gsmtap_msg, addr = self.gsmtap.read_packet()
|
||||||
if gsmtap_msg['type'] != 'sim':
|
if gsmtap_msg['type'] != 'sim':
|
||||||
raise ValueError('Unsupported GSMTAP type %s' % gsmtap_msg['type'])
|
raise ValueError('Unsupported GSMTAP type %s' % gsmtap_msg['type'])
|
||||||
sub_type = gsmtap_msg['sub_type']
|
sub_type = gsmtap_msg['sub_type']
|
||||||
if sub_type == 'apdu':
|
if sub_type == 'apdu':
|
||||||
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
|
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
|
||||||
if sub_type == 'atr':
|
elif sub_type == 'atr':
|
||||||
# card has been reset
|
# card has been reset
|
||||||
return CardReset(gsmtap_msg['body'])
|
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
|
# simply ignore for now
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -16,20 +16,21 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
from pprint import pprint as pp
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import pyshark
|
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
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -66,10 +67,10 @@ class _PysharkGsmtap(ApduSource):
|
|||||||
sub_type = gsmtap_msg['sub_type']
|
sub_type = gsmtap_msg['sub_type']
|
||||||
if sub_type == 'apdu':
|
if sub_type == 'apdu':
|
||||||
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
|
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
|
||||||
if sub_type == 'atr':
|
elif sub_type == 'atr':
|
||||||
# card has been reset
|
# card has been reset
|
||||||
return CardReset(gsmtap_msg['body'])
|
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
|
# simply ignore for now
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -86,3 +87,4 @@ class PysharkGsmtapPcap(_PysharkGsmtap):
|
|||||||
"""
|
"""
|
||||||
pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='gsm_sim', 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)
|
super().__init__(pyshark_inst)
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,13 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
from pprint import pprint as pp
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import pyshark
|
import pyshark
|
||||||
|
|
||||||
from pySim.utils import h2b
|
from pySim.utils import h2b, b2h
|
||||||
from pySim.apdu import Tpdu
|
from pySim.apdu import Tpdu
|
||||||
from . import ApduSource, PacketType, CardReset
|
from . import ApduSource, PacketType, CardReset
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
27
pySim/app.py
27
pySim/app.py
@@ -19,13 +19,13 @@ from typing import Tuple
|
|||||||
from pySim.transport import LinkBase
|
from pySim.transport import LinkBase
|
||||||
from pySim.commands import SimCardCommands
|
from pySim.commands import SimCardCommands
|
||||||
from pySim.filesystem import CardModel, CardApplication
|
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.runtime import RuntimeState
|
||||||
from pySim.profile import CardProfile
|
from pySim.profile import CardProfile
|
||||||
from pySim.cdma_ruim import CardProfileRUIM
|
from pySim.cdma_ruim import CardProfileRUIM
|
||||||
from pySim.ts_102_221 import CardProfileUICC
|
from pySim.ts_102_221 import CardProfileUICC
|
||||||
from pySim.utils import all_subclasses
|
from pySim.utils import all_subclasses
|
||||||
from pySim.exceptions import SwMatchError
|
|
||||||
|
|
||||||
# we need to import this module so that the SysmocomSJA2 sub-class of
|
# we need to import this module so that the SysmocomSJA2 sub-class of
|
||||||
# CardModel is created, which will add the ATR-based matching and
|
# 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.global_platform
|
||||||
import pySim.euicc
|
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
|
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
|
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...")
|
print("Waiting for card...")
|
||||||
sl.wait_for_card(3)
|
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
|
generic_card = False
|
||||||
card = card_detect(scc)
|
card = card_detect(scc)
|
||||||
if card is None:
|
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
|
# inform the transport that we can do context-specific SW interpretation
|
||||||
sl.set_sw_interpreter(rs)
|
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
|
return rs, card
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
249
pySim/ara_m.py
249
pySim/ara_m.py
@@ -26,29 +26,27 @@ 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 construct import Optional as COptional
|
||||||
from osmocom.construct import *
|
from pySim.construct import *
|
||||||
from osmocom.tlv import *
|
|
||||||
from osmocom.utils import Hexstr
|
|
||||||
from pySim.filesystem import *
|
from pySim.filesystem import *
|
||||||
import pySim.global_platform
|
from pySim.tlv import *
|
||||||
|
|
||||||
# various BER-TLV encoded Data Objects (DOs)
|
# various BER-TLV encoded Data Objects (DOs)
|
||||||
|
|
||||||
|
|
||||||
class AidRefDO(BER_TLV_IE, tag=0x4f):
|
class AidRefDO(BER_TLV_IE, tag=0x4f):
|
||||||
# GPD_SPE_013 v1.1 Table 6-3
|
# SEID v1.1 Table 6-3
|
||||||
_construct = HexAdapter(GreedyBytes)
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
|
|
||||||
class AidRefEmptyDO(BER_TLV_IE, tag=0xc0):
|
class AidRefEmptyDO(BER_TLV_IE, tag=0xc0):
|
||||||
# GPD_SPE_013 v1.1 Table 6-3
|
# SEID v1.1 Table 6-3
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DevAppIdRefDO(BER_TLV_IE, tag=0xc1):
|
class DevAppIdRefDO(BER_TLV_IE, tag=0xc1):
|
||||||
# GPD_SPE_013 v1.1 Table 6-4
|
# SEID v1.1 Table 6-4
|
||||||
_construct = HexAdapter(GreedyBytes)
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
|
|
||||||
@@ -58,39 +56,41 @@ class PkgRefDO(BER_TLV_IE, tag=0xca):
|
|||||||
|
|
||||||
|
|
||||||
class RefDO(BER_TLV_IE, tag=0xe1, nested=[AidRefDO, AidRefEmptyDO, DevAppIdRefDO, PkgRefDO]):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ApduArDO(BER_TLV_IE, tag=0xd0):
|
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):
|
def _from_bytes(self, do: bytes):
|
||||||
if len(do) == 1:
|
if len(do) == 1:
|
||||||
if do[0] == 0x00:
|
if do[0] == 0x00:
|
||||||
self.decoded = {'generic_access_rule': 'never'}
|
self.decoded = {'generic_access_rule': 'never'}
|
||||||
return self.decoded
|
return self.decoded
|
||||||
if do[0] == 0x01:
|
elif do[0] == 0x01:
|
||||||
self.decoded = {'generic_access_rule': 'always'}
|
self.decoded = {'generic_access_rule': 'always'}
|
||||||
return self.decoded
|
return self.decoded
|
||||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
else:
|
||||||
|
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||||
else:
|
else:
|
||||||
if len(do) % 8:
|
if len(do) % 8:
|
||||||
return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
|
return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
|
||||||
self.decoded = {'apdu_filter': []}
|
self.decoded['apdu_filter'] = []
|
||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(do):
|
while offset < len(do):
|
||||||
self.decoded['apdu_filter'] += [{'header': b2h(do[offset:offset+4]),
|
self.decoded['apdu_filter'] += {'header': b2h(do[offset:offset+4]),
|
||||||
'mask': b2h(do[offset+4:offset+8])}]
|
'mask': b2h(do[offset+4:offset+8])}
|
||||||
offset += 8 # Move offset to the beginning of the next apdu_filter object
|
self.decoded = res
|
||||||
return self.decoded
|
return res
|
||||||
|
|
||||||
def _to_bytes(self):
|
def _to_bytes(self):
|
||||||
if 'generic_access_rule' in self.decoded:
|
if 'generic_access_rule' in self.decoded:
|
||||||
if self.decoded['generic_access_rule'] == 'never':
|
if self.decoded['generic_access_rule'] == 'never':
|
||||||
return b'\x00'
|
return b'\x00'
|
||||||
if self.decoded['generic_access_rule'] == 'always':
|
elif self.decoded['generic_access_rule'] == 'always':
|
||||||
return b'\x01'
|
return b'\x01'
|
||||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
else:
|
||||||
|
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||||
else:
|
else:
|
||||||
if not 'apdu_filter' in self.decoded:
|
if not 'apdu_filter' in self.decoded:
|
||||||
return ValueError('Invalid APDU AR DO')
|
return ValueError('Invalid APDU AR DO')
|
||||||
@@ -108,134 +108,135 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
|
|||||||
|
|
||||||
|
|
||||||
class NfcArDO(BER_TLV_IE, tag=0xd1):
|
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' /
|
_construct = Struct('nfc_event_access_rule' /
|
||||||
Enum(Int8ub, never=0, always=1))
|
Enum(Int8ub, never=0, always=1))
|
||||||
|
|
||||||
|
|
||||||
class PermArDO(BER_TLV_IE, tag=0xdb):
|
class PermArDO(BER_TLV_IE, tag=0xdb):
|
||||||
# Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
|
# Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
|
||||||
# based on Table 6-8 of GlobalPlatform Device API Access Control v1.0
|
|
||||||
_construct = Struct('permissions'/HexAdapter(Bytes(8)))
|
_construct = Struct('permissions'/HexAdapter(Bytes(8)))
|
||||||
|
|
||||||
|
|
||||||
class ArDO(BER_TLV_IE, tag=0xe3, nested=[ApduArDO, NfcArDO, PermArDO]):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RefArDO(BER_TLV_IE, tag=0xe2, nested=[RefDO, ArDO]):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ResponseAllRefArDO(BER_TLV_IE, tag=0xff40, nested=[RefArDO]):
|
class ResponseAllRefArDO(BER_TLV_IE, tag=0xff40, nested=[RefArDO]):
|
||||||
# GPD_SPE_013 v1.1 Table 4-2
|
# SEID v1.1 Table 4-2
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ResponseArDO(BER_TLV_IE, tag=0xff50, nested=[ArDO]):
|
class ResponseArDO(BER_TLV_IE, tag=0xff50, nested=[ArDO]):
|
||||||
# GPD_SPE_013 v1.1 Table 4-3
|
# SEID v1.1 Table 4-3
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ResponseRefreshTagDO(BER_TLV_IE, tag=0xdf20):
|
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)))
|
_construct = Struct('refresh_tag'/HexAdapter(Bytes(8)))
|
||||||
|
|
||||||
|
|
||||||
class DeviceInterfaceVersionDO(BER_TLV_IE, tag=0xe6):
|
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)
|
_construct = Struct('major'/Int8ub, 'minor'/Int8ub, 'patch'/Int8ub)
|
||||||
|
|
||||||
|
|
||||||
class DeviceConfigDO(BER_TLV_IE, tag=0xe4, nested=[DeviceInterfaceVersionDO]):
|
class DeviceConfigDO(BER_TLV_IE, tag=0xe4, nested=[DeviceInterfaceVersionDO]):
|
||||||
# GPD_SPE_013 v1.1 Table 6-10
|
# SEID v1.1 Table 6-10
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ResponseDeviceConfigDO(BER_TLV_IE, tag=0xff7f, nested=[DeviceConfigDO]):
|
class ResponseDeviceConfigDO(BER_TLV_IE, tag=0xff7f, nested=[DeviceConfigDO]):
|
||||||
# GPD_SPE_013 v1.1 Table 5-14
|
# SEID v1.1 Table 5-14
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AramConfigDO(BER_TLV_IE, tag=0xe5, nested=[DeviceInterfaceVersionDO]):
|
class AramConfigDO(BER_TLV_IE, tag=0xe5, nested=[DeviceInterfaceVersionDO]):
|
||||||
# GPD_SPE_013 v1.1 Table 6-11
|
# SEID v1.1 Table 6-11
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ResponseAramConfigDO(BER_TLV_IE, tag=0xdf21, nested=[AramConfigDO]):
|
class ResponseAramConfigDO(BER_TLV_IE, tag=0xdf21, nested=[AramConfigDO]):
|
||||||
# GPD_SPE_013 v1.1 Table 4-5
|
# SEID v1.1 Table 4-5
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandStoreRefArDO(BER_TLV_IE, tag=0xf0, nested=[RefArDO]):
|
class CommandStoreRefArDO(BER_TLV_IE, tag=0xf0, nested=[RefArDO]):
|
||||||
# GPD_SPE_013 v1.1 Table 5-2
|
# SEID v1.1 Table 5-2
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandDelete(BER_TLV_IE, tag=0xf1, nested=[AidRefDO, AidRefEmptyDO, RefDO, RefArDO]):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandUpdateRefreshTagDO(BER_TLV_IE, tag=0xf2):
|
class CommandUpdateRefreshTagDO(BER_TLV_IE, tag=0xf2):
|
||||||
# GPD_SPE_013 V1.1 Table 5-6
|
# SEID V1.1 Table 5-6
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandRegisterClientAidsDO(BER_TLV_IE, tag=0xf7, nested=[AidRefDO, AidRefEmptyDO]):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandGet(BER_TLV_IE, tag=0xf3, nested=[AidRefDO, AidRefEmptyDO]):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandGetAll(BER_TLV_IE, tag=0xf4):
|
class CommandGetAll(BER_TLV_IE, tag=0xf4):
|
||||||
# GPD_SPE_013 v1.1 Table 5-9
|
# SEID v1.1 Table 5-9
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandGetClientAidsDO(BER_TLV_IE, tag=0xf6):
|
class CommandGetClientAidsDO(BER_TLV_IE, tag=0xf6):
|
||||||
# GPD_SPE_013 v1.1 Table 5-10
|
# SEID v1.1 Table 5-10
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandGetNext(BER_TLV_IE, tag=0xf5):
|
class CommandGetNext(BER_TLV_IE, tag=0xf5):
|
||||||
# GPD_SPE_013 v1.1 Table 5-11
|
# SEID v1.1 Table 5-11
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandGetDeviceConfigDO(BER_TLV_IE, tag=0xf8):
|
class CommandGetDeviceConfigDO(BER_TLV_IE, tag=0xf8):
|
||||||
# GPD_SPE_013 v1.1 Table 5-12
|
# SEID v1.1 Table 5-12
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ResponseAracAidDO(BER_TLV_IE, tag=0xff70, nested=[AidRefDO, AidRefEmptyDO]):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BlockDO(BER_TLV_IE, tag=0xe7):
|
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)
|
_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]):
|
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# SEID v1.1 Table 4-2
|
||||||
|
|
||||||
|
|
||||||
# GPD_SPE_013 v1.1 Table 4-2
|
|
||||||
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
|
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
|
||||||
ResponseRefreshTagDO, ResponseAramConfigDO]):
|
ResponseRefreshTagDO, ResponseAramConfigDO]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# SEID v1.1 Table 5-1
|
||||||
|
|
||||||
|
|
||||||
# GPD_SPE_013 v1.1 Table 5-1
|
|
||||||
class StoreCommandDoCollection(TLV_IE_Collection,
|
class StoreCommandDoCollection(TLV_IE_Collection,
|
||||||
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
|
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
|
||||||
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
|
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
|
||||||
@@ -244,7 +245,7 @@ class StoreCommandDoCollection(TLV_IE_Collection,
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# GPD_SPE_013 v1.1 Section 5.1.2
|
# SEID v1.1 Section 5.1.2
|
||||||
class StoreResponseDoCollection(TLV_IE_Collection,
|
class StoreResponseDoCollection(TLV_IE_Collection,
|
||||||
nested=[ResponseAllRefArDO, ResponseAracAidDO, ResponseDeviceConfigDO]):
|
nested=[ResponseAllRefArDO, ResponseAracAidDO, ResponseDeviceConfigDO]):
|
||||||
pass
|
pass
|
||||||
@@ -258,11 +259,8 @@ class ADF_ARAM(CardADF):
|
|||||||
files = []
|
files = []
|
||||||
self.add_files(files)
|
self.add_files(files)
|
||||||
|
|
||||||
def decode_select_response(self, data_hex):
|
|
||||||
return pySim.global_platform.decode_select_response(data_hex)
|
|
||||||
|
|
||||||
@staticmethod
|
@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
|
"""Transceive an APDU with the card, transparently encoding the command data from TLV
|
||||||
and decoding the response data tlv."""
|
and decoding the response data tlv."""
|
||||||
if cmd_do:
|
if cmd_do:
|
||||||
@@ -274,55 +272,59 @@ class ADF_ARAM(CardADF):
|
|||||||
cmd_do_enc = b''
|
cmd_do_enc = b''
|
||||||
cmd_do_len = 0
|
cmd_do_len = 0
|
||||||
c_apdu = hdr + ('%02x' % cmd_do_len) + b2h(cmd_do_enc)
|
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 data:
|
||||||
if resp_cls:
|
if resp_cls:
|
||||||
resp_do = resp_cls()
|
resp_do = resp_cls()
|
||||||
resp_do.from_tlv(h2b(data))
|
resp_do.from_tlv(h2b(data))
|
||||||
return resp_do
|
return resp_do
|
||||||
return data
|
else:
|
||||||
|
return data
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def store_data(scc, do) -> bytes:
|
def store_data(tp, do) -> bytes:
|
||||||
"""Build the Command APDU for STORE DATA."""
|
"""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
|
@staticmethod
|
||||||
def get_all(scc):
|
def get_all(tp):
|
||||||
return ADF_ARAM.xceive_apdu_tlv(scc, '80caff40', None, GetResponseDoCollection)
|
return ADF_ARAM.xceive_apdu_tlv(tp, '80caff40', None, GetResponseDoCollection)
|
||||||
|
|
||||||
@staticmethod
|
@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 = DeviceConfigDO()
|
||||||
cmd_do.from_val_dict([{'device_interface_version_do': {
|
cmd_do.from_dict([{'device_interface_version_do': {
|
||||||
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
|
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
|
||||||
return ADF_ARAM.xceive_apdu_tlv(scc, '80cadf21', cmd_do, ResponseAramConfigDO)
|
return ADF_ARAM.xceive_apdu_tlv(tp, '80cadf21', cmd_do, ResponseAramConfigDO)
|
||||||
|
|
||||||
@with_default_category('Application-Specific Commands')
|
@with_default_category('Application-Specific Commands')
|
||||||
class AddlShellCommands(CommandSet):
|
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"""
|
"""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:
|
if res_do:
|
||||||
self._cmd.poutput_json(res_do.to_dict())
|
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."""
|
"""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:
|
if res_do:
|
||||||
self._cmd.poutput_json(res_do.to_dict())
|
self._cmd.poutput_json(res_do.to_dict())
|
||||||
|
|
||||||
store_ref_ar_do_parse = argparse.ArgumentParser()
|
store_ref_ar_do_parse = argparse.ArgumentParser()
|
||||||
# REF-DO
|
# REF-DO
|
||||||
store_ref_ar_do_parse.add_argument(
|
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 = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||||
aid_grp.add_argument(
|
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',
|
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(
|
store_ref_ar_do_parse.add_argument(
|
||||||
'--pkg-ref', help='Full Android Java package name (up to 127 chars ASCII)')
|
'--pkg-ref', help='Full Android Java package name (up to 127 chars ASCII)')
|
||||||
# AR-DO
|
# AR-DO
|
||||||
@@ -332,7 +334,7 @@ class ADF_ARAM(CardADF):
|
|||||||
apdu_grp.add_argument(
|
apdu_grp.add_argument(
|
||||||
'--apdu-always', action='store_true', help='APDU access is allowed')
|
'--apdu-always', action='store_true', help='APDU access is allowed')
|
||||||
apdu_grp.add_argument(
|
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 = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||||
nfc_grp.add_argument('--nfc-always', action='store_true',
|
nfc_grp.add_argument('--nfc-always', action='store_true',
|
||||||
help='NFC event access is allowed')
|
help='NFC event access is allowed')
|
||||||
@@ -346,7 +348,7 @@ class ADF_ARAM(CardADF):
|
|||||||
"""Perform STORE DATA [Command-Store-REF-AR-DO] to store a (new) access rule."""
|
"""Perform STORE DATA [Command-Store-REF-AR-DO] to store a (new) access rule."""
|
||||||
# REF
|
# REF
|
||||||
ref_do_content = []
|
ref_do_content = []
|
||||||
if opts.aid is not None:
|
if opts.aid:
|
||||||
ref_do_content += [{'aid_ref_do': opts.aid}]
|
ref_do_content += [{'aid_ref_do': opts.aid}]
|
||||||
elif opts.aid_empty:
|
elif opts.aid_empty:
|
||||||
ref_do_content += [{'aid_ref_empty_do': None}]
|
ref_do_content += [{'aid_ref_empty_do': None}]
|
||||||
@@ -356,19 +358,12 @@ class ADF_ARAM(CardADF):
|
|||||||
# AR
|
# AR
|
||||||
ar_do_content = []
|
ar_do_content = []
|
||||||
if opts.apdu_never:
|
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:
|
elif opts.apdu_always:
|
||||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
|
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
|
||||||
elif opts.apdu_filter:
|
elif opts.apdu_filter:
|
||||||
if len(opts.apdu_filter) % 16:
|
# TODO: multiple filters
|
||||||
return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do))
|
ar_do_content += [{'apdu_ar_do': {'apdu_filter': [opts.apdu_filter]}}]
|
||||||
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}}]
|
|
||||||
if opts.nfc_always:
|
if opts.nfc_always:
|
||||||
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
|
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
|
||||||
elif opts.nfc_never:
|
elif opts.nfc_never:
|
||||||
@@ -377,29 +372,24 @@ class ADF_ARAM(CardADF):
|
|||||||
ar_do_content += [{'perm_ar_do': {'permissions': opts.android_permissions}}]
|
ar_do_content += [{'perm_ar_do': {'permissions': opts.android_permissions}}]
|
||||||
d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
|
d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
|
||||||
csrado = CommandStoreRefArDO()
|
csrado = CommandStoreRefArDO()
|
||||||
csrado.from_val_dict(d)
|
csrado.from_dict(d)
|
||||||
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, csrado)
|
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, csrado)
|
||||||
if res_do:
|
if res_do:
|
||||||
self._cmd.poutput_json(res_do.to_dict())
|
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."""
|
"""Perform STORE DATA [Command-Delete[all]] to delete all access rules."""
|
||||||
deldo = CommandDelete()
|
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:
|
if res_do:
|
||||||
self._cmd.poutput_json(res_do.to_dict())
|
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 Martel’s ARA-M implementation.)"""
|
|
||||||
self._cmd.lchan.scc.send_apdu_checksw('80e2900001A1', '9000')
|
|
||||||
|
|
||||||
|
|
||||||
# SEAC v1.1 Section 4.1.2.2 + 5.1.2.2
|
# SEAC v1.1 Section 4.1.2.2 + 5.1.2.2
|
||||||
sw_aram = {
|
sw_aram = {
|
||||||
'ARA-M': {
|
'ARA-M': {
|
||||||
'6381': 'Rule successfully stored but an access rule already exists',
|
'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',
|
'6581': 'Memory Problem',
|
||||||
'6700': 'Wrong Length in Lc',
|
'6700': 'Wrong Length in Lc',
|
||||||
'6981': 'DO is not supported by the ARA-M/ARA-C',
|
'6981': 'DO is not supported by the ARA-M/ARA-C',
|
||||||
@@ -420,84 +410,3 @@ sw_aram = {
|
|||||||
class CardApplicationARAM(CardApplication):
|
class CardApplicationARAM(CardApplication):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('ARA-M', adf=ADF_ARAM(), sw=sw_aram)
|
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()
|
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ there are also automatic card feeders.
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from pySim.transport import LinkBase
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from pySim.transport import LinkBase
|
|
||||||
|
|
||||||
class CardHandlerBase:
|
class CardHandlerBase:
|
||||||
"""Abstract base class representing a mechanism for card insertion/removal."""
|
"""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))
|
print("Card handler Config-file: " + str(config_file))
|
||||||
with open(config_file) as cfg:
|
with open(config_file) as cfg:
|
||||||
self.cmds = yaml.load(cfg, Loader=yaml.FullLoader)
|
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):
|
def __print_outout(self, out):
|
||||||
print("")
|
print("")
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ the need of manually entering the related card-individual data on every
|
|||||||
operation with pySim-shell.
|
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
|
# All Rights Reserved
|
||||||
#
|
#
|
||||||
# Author: Philipp Maier, Harald Welte
|
# Author: Philipp Maier
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -29,227 +29,95 @@ operation with pySim-shell.
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from typing import List, Dict, Optional
|
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 abc
|
||||||
import csv
|
import csv
|
||||||
import logging
|
|
||||||
import yaml
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.sql import Identifier, SQL
|
|
||||||
|
|
||||||
log = PySimLogger.get("CARDKEY")
|
|
||||||
|
|
||||||
card_key_providers = [] # type: List['CardKeyProvider']
|
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_ISDR', '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):
|
class CardKeyProvider(abc.ABC):
|
||||||
"""Base class, not containing any concrete implementation."""
|
"""Base class, not containing any concrete implementation."""
|
||||||
|
|
||||||
@abc.abstractmethod
|
VALID_FIELD_NAMES = ['ICCID', 'ADM1',
|
||||||
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
|
'IMSI', 'PIN1', 'PIN2', 'PUK1', 'PUK2']
|
||||||
"""
|
|
||||||
Get multiple card-individual fields for identified card. This method should not fail with an exception in
|
# check input parameters, but do nothing concrete yet
|
||||||
case the entry, columns or even the key column itsself is not found.
|
def _verify_get_data(self, fields: List[str] = [], key: str = 'ICCID', value: str = "") -> Dict[str, str]:
|
||||||
|
"""Verify multiple fields for identified card.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
fields : list of valid field names such as 'ADM1', 'PIN1', ... which are to be obtained
|
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'
|
key : look-up key to identify card data, such as 'ICCID'
|
||||||
value : value for look-up key to identify card data
|
value : value for look-up key to identify card data
|
||||||
Returns:
|
Returns:
|
||||||
dictionary of {field : value, ...} strings for each requested field from 'fields'. In case nothing is
|
dictionary of {field, value} strings for each requested field from 'fields'
|
||||||
fond None shall be returned.
|
"""
|
||||||
|
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):
|
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:
|
Args:
|
||||||
csv_filename : file name (path) of CSV file containing card-individual key/data
|
filename : file name (path) of CSV file containing card-individual key/data
|
||||||
transport_keys : (see class CardKeyFieldCryptor)
|
|
||||||
"""
|
"""
|
||||||
log.info("Using CSV file as card key data source: %s" % csv_filename)
|
self.csv_file = open(filename, 'r')
|
||||||
self.csv_file = open(csv_filename, 'r')
|
|
||||||
if not self.csv_file:
|
if not self.csv_file:
|
||||||
raise RuntimeError("Could not open CSV file '%s'" % csv_filename)
|
raise RuntimeError("Could not open CSV file '%s'" % filename)
|
||||||
self.csv_filename = csv_filename
|
self.filename = filename
|
||||||
self.crypt = CardKeyFieldCryptor(transport_keys)
|
|
||||||
|
|
||||||
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
|
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)
|
self.csv_file.seek(0)
|
||||||
cr = csv.DictReader(self.csv_file)
|
cr = csv.DictReader(self.csv_file)
|
||||||
if not cr:
|
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]
|
cr.fieldnames = [field.upper() for field in cr.fieldnames]
|
||||||
|
|
||||||
if key not in cr.fieldnames:
|
rc = {}
|
||||||
return None
|
|
||||||
return_dict = {}
|
|
||||||
for row in cr:
|
for row in cr:
|
||||||
if row[key] == value:
|
if row[key] == value:
|
||||||
for f in fields:
|
for f in fields:
|
||||||
if f in row:
|
if f in row:
|
||||||
return_dict.update({f: self.crypt.decrypt_field(f, row[f])})
|
rc.update({f: row[f]})
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("CSV-File '%s' lacks column '%s'" % (self.csv_filename, f))
|
raise RuntimeError("CSV-File '%s' lacks column '%s'" %
|
||||||
if return_dict == {}:
|
(self.filename, f))
|
||||||
return None
|
return rc
|
||||||
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)
|
|
||||||
"""
|
|
||||||
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]:
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def card_key_provider_register(provider: CardKeyProvider, provider_list=card_key_providers):
|
def card_key_provider_register(provider: CardKeyProvider, provider_list=card_key_providers):
|
||||||
@@ -260,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
|
provider_list : override the list of providers from the global default
|
||||||
"""
|
"""
|
||||||
if not isinstance(provider, CardKeyProvider):
|
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)
|
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.
|
"""Query all registered card data providers for card-individual [key] data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -275,21 +143,17 @@ def card_key_provider_get(fields: list[str], key: str, value: str, provider_list
|
|||||||
Returns:
|
Returns:
|
||||||
dictionary of {field, value} strings for each requested field from 'fields'
|
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:
|
for p in provider_list:
|
||||||
if not isinstance(p, CardKeyProvider):
|
if not isinstance(p, CardKeyProvider):
|
||||||
raise ValueError("Provider list contains element which is not a card data provider")
|
raise ValueError(
|
||||||
log.debug("Searching for card key data (key=%s, value=%s, provider=%s)" % (key, value, str(p)))
|
"provider list contains element which is not a card data provier")
|
||||||
result = p.get(fields, key, value)
|
result = p.get(fields, key, value)
|
||||||
if result:
|
if result:
|
||||||
log.debug("Found card data: %s" % (str(result)))
|
|
||||||
return result
|
return result
|
||||||
|
return {}
|
||||||
raise ValueError("Unable to find card key data (key=%s, value=%s, fields=%s)" % (key, value, str(fields)))
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
"""Query all registered card data providers for a single field.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -300,7 +164,11 @@ def card_key_provider_get_field(field: str, key: str, value: str, provider_list=
|
|||||||
Returns:
|
Returns:
|
||||||
dictionary of {field, value} strings for the requested field
|
dictionary of {field, value} strings for the requested field
|
||||||
"""
|
"""
|
||||||
|
for p in provider_list:
|
||||||
fields = [field]
|
if not isinstance(p, CardKeyProvider):
|
||||||
result = card_key_provider_get(fields, key, value, card_key_providers)
|
raise ValueError(
|
||||||
return result.get(field.upper())
|
"provider list contains element which is not a card data provier")
|
||||||
|
result = p.get_field(field, key, value)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
return None
|
||||||
|
|||||||
@@ -22,12 +22,12 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Dict, Tuple
|
||||||
from osmocom.utils import *
|
from pySim.ts_102_221 import EF_DIR
|
||||||
|
|
||||||
from pySim.ts_102_221 import EF_DIR, CardProfileUICC
|
|
||||||
from pySim.ts_51_011 import DF_GSM
|
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
|
from pySim.commands import Path, SimCardCommands
|
||||||
|
|
||||||
class CardBase:
|
class CardBase:
|
||||||
@@ -40,7 +40,8 @@ class CardBase:
|
|||||||
rc = self._scc.reset_card()
|
rc = self._scc.reset_card()
|
||||||
if rc == 1:
|
if rc == 1:
|
||||||
return self._scc.get_atr()
|
return self._scc.get_atr()
|
||||||
return None
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def set_apdu_parameter(self, cla: Hexstr, sel_ctrl: Hexstr) -> None:
|
def set_apdu_parameter(self, cla: Hexstr, sel_ctrl: Hexstr) -> None:
|
||||||
"""Set apdu parameters (class byte and selection control bytes)"""
|
"""Set apdu parameters (class byte and selection control bytes)"""
|
||||||
@@ -53,19 +54,13 @@ class CardBase:
|
|||||||
|
|
||||||
def erase(self):
|
def erase(self):
|
||||||
print("warning: erasing is not supported for specified card type!")
|
print("warning: erasing is not supported for specified card type!")
|
||||||
|
return
|
||||||
|
|
||||||
def file_exists(self, fid: Path) -> bool:
|
def file_exists(self, fid: Path) -> bool:
|
||||||
"""Determine if the file exists (and is not deactivated)."""
|
|
||||||
res_arr = self._scc.try_select_path(fid)
|
res_arr = self._scc.try_select_path(fid)
|
||||||
for res in res_arr:
|
for res in res_arr:
|
||||||
if res[1] != '9000':
|
if res[1] != '9000':
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
def read_aids(self) -> List[Hexstr]:
|
def read_aids(self) -> List[Hexstr]:
|
||||||
@@ -73,16 +68,6 @@ class CardBase:
|
|||||||
# callers having to do hasattr('read_aids') ahead of every call.
|
# callers having to do hasattr('read_aids') ahead of every call.
|
||||||
return []
|
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):
|
class SimCardBase(CardBase):
|
||||||
"""Here we only add methods for commands specified in TS 51.011, without
|
"""Here we only add methods for commands specified in TS 51.011, without
|
||||||
@@ -90,7 +75,7 @@ class SimCardBase(CardBase):
|
|||||||
name = 'SIM'
|
name = 'SIM'
|
||||||
|
|
||||||
def __init__(self, scc: SimCardCommands):
|
def __init__(self, scc: SimCardCommands):
|
||||||
super().__init__(scc)
|
super(SimCardBase, self).__init__(scc)
|
||||||
self._scc.cla_byte = "A0"
|
self._scc.cla_byte = "A0"
|
||||||
self._scc.sel_ctrl = "0000"
|
self._scc.sel_ctrl = "0000"
|
||||||
|
|
||||||
@@ -103,7 +88,7 @@ class UiccCardBase(SimCardBase):
|
|||||||
name = 'UICC'
|
name = 'UICC'
|
||||||
|
|
||||||
def __init__(self, scc: SimCardCommands):
|
def __init__(self, scc: SimCardCommands):
|
||||||
super().__init__(scc)
|
super(UiccCardBase, self).__init__(scc)
|
||||||
self._scc.cla_byte = "00"
|
self._scc.cla_byte = "00"
|
||||||
self._scc.sel_ctrl = "0004" # request an FCP
|
self._scc.sel_ctrl = "0004" # request an FCP
|
||||||
# See also: ETSI TS 102 221, Table 9.3
|
# See also: ETSI TS 102 221, Table 9.3
|
||||||
@@ -112,8 +97,6 @@ class UiccCardBase(SimCardBase):
|
|||||||
def probe(self) -> bool:
|
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 is a mandatory EF on all ICCIDs; however it *may* also exist on a TS 51.011 SIM
|
||||||
ef_dir = EF_DIR()
|
ef_dir = EF_DIR()
|
||||||
# select MF first
|
|
||||||
self.file_exists("3f00")
|
|
||||||
return self.file_exists(ef_dir.fid)
|
return self.file_exists(ef_dir.fid)
|
||||||
|
|
||||||
def read_aids(self) -> List[Hexstr]:
|
def read_aids(self) -> List[Hexstr]:
|
||||||
@@ -175,8 +158,9 @@ class UiccCardBase(SimCardBase):
|
|||||||
aid_full = self._complete_aid(aid)
|
aid_full = self._complete_aid(aid)
|
||||||
if aid_full:
|
if aid_full:
|
||||||
return scc.select_adf(aid_full)
|
return scc.select_adf(aid_full)
|
||||||
# If we cannot get the full AID, try with short AID
|
else:
|
||||||
return scc.select_adf(aid)
|
# If we cannot get the full AID, try with short AID
|
||||||
|
return scc.select_adf(aid)
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
def card_detect(scc: SimCardCommands) -> Optional[CardBase]:
|
def card_detect(scc: SimCardCommands) -> Optional[CardBase]:
|
||||||
|
|||||||
412
pySim/cat.py
412
pySim/cat.py
@@ -18,58 +18,43 @@ as described in 3GPP TS 31.111."""
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from typing import List
|
|
||||||
from bidict import bidict
|
from bidict import bidict
|
||||||
from construct import Int8ub, Int16ub, Byte, BitsInteger
|
from typing import List
|
||||||
from construct import Struct, Enum, BitStruct, this
|
from pySim.utils import b2h, h2b, dec_xplmn_w_act
|
||||||
from construct import Switch, GreedyRange, FlagsEnum
|
from pySim.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
|
||||||
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
|
from pySim.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi
|
||||||
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
|
from construct import Int8ub, Int16ub, Byte, Bytes, Bit, Flag, BitsInteger
|
||||||
from osmocom.utils import b2h, h2b
|
from construct import Struct, Enum, Tell, BitStruct, this, Padding, RepeatUntil
|
||||||
from pySim.utils import dec_xplmn_w_act
|
from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
|
||||||
|
|
||||||
# Tag values as per TS 101 220 Table 7.23
|
# Tag values as per TS 101 220 Table 7.23
|
||||||
|
|
||||||
# TS 102 223 Section 8.1
|
# TS 102 223 Section 8.1
|
||||||
class Address(COMPR_TLV_IE, tag=0x86):
|
class Address(COMPR_TLV_IE, tag=0x06):
|
||||||
_construct = Struct('ton_npi'/TonNpi,
|
_construct = Struct('ton_npi'/Int8ub,
|
||||||
'call_number'/BcdAdapter(GreedyBytes))
|
'call_number'/BcdAdapter(Bytes(this._.total_len-1)))
|
||||||
|
|
||||||
# TS 102 223 Section 8.2
|
# TS 102 223 Section 8.2
|
||||||
class AlphaIdentifier(COMPR_TLV_IE, tag=0x85):
|
class AlphaIdentifier(COMPR_TLV_IE, tag=0x05):
|
||||||
# FIXME: like EF.ADN
|
# FIXME: like EF.ADN
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 8.3
|
# TS 102 223 Section 8.3
|
||||||
class Subaddress(COMPR_TLV_IE, tag=0x88):
|
class Subaddress(COMPR_TLV_IE, tag=0x08):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 8.4 + TS 31.111 Section 8.4
|
# 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
|
pass
|
||||||
|
|
||||||
# TS 31.111 Section 8.5
|
# TS 31.111 Section 8.5
|
||||||
class CBSPage(COMPR_TLV_IE, tag=0x8C):
|
class CBSPage(COMPR_TLV_IE, tag=0x0C):
|
||||||
pass
|
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
|
# TS 102 223 Section 8.6 + TS 31.111 Section 8.6
|
||||||
class CommandDetails(COMPR_TLV_IE, tag=0x81):
|
class CommandDetails(COMPR_TLV_IE, tag=0x81):
|
||||||
_construct = Struct('command_number'/Int8ub,
|
_construct = Struct('command_number'/Int8ub,
|
||||||
'type_of_command'/TypeOfCommand,
|
'type_of_command'/Int8ub,
|
||||||
'command_qualifier'/Int8ub)
|
'command_qualifier'/Int8ub)
|
||||||
|
|
||||||
# TS 102 223 Section 8.7
|
# TS 102 223 Section 8.7
|
||||||
@@ -122,26 +107,26 @@ class DeviceIdentities(COMPR_TLV_IE, tag=0x82):
|
|||||||
return bytes([src, dst])
|
return bytes([src, dst])
|
||||||
|
|
||||||
# TS 102 223 Section 8.8
|
# 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),
|
_construct = Struct('time_unit'/Enum(Int8ub, minutes=0, seconds=1, tenths_of_seconds=2),
|
||||||
'time_interval'/Int8ub)
|
'time_interval'/Int8ub)
|
||||||
|
|
||||||
# TS 102 223 Section 8.9
|
# TS 102 223 Section 8.9
|
||||||
class Item(COMPR_TLV_IE, tag=0x8f):
|
class Item(COMPR_TLV_IE, tag=0x0f):
|
||||||
_construct = Struct('identifier'/Int8ub,
|
_construct = Struct('identifier'/Int8ub,
|
||||||
'text_string'/GsmStringAdapter(GreedyBytes))
|
'text_string'/GsmStringAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.10
|
# TS 102 223 Section 8.10
|
||||||
class ItemIdentifier(COMPR_TLV_IE, tag=0x90):
|
class ItemIdentifier(COMPR_TLV_IE, tag=0x10):
|
||||||
_construct = Struct('identifier'/Int8ub)
|
_construct = Struct('identifier'/Int8ub)
|
||||||
|
|
||||||
# TS 102 223 Section 8.11
|
# 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,
|
_construct = Struct('minimum_length'/Int8ub,
|
||||||
'maximum_length'/Int8ub)
|
'maximum_length'/Int8ub)
|
||||||
|
|
||||||
# TS 102 223 Section 8.12
|
# TS 102 223 Section 8.12
|
||||||
class Result(COMPR_TLV_IE, tag=0x83):
|
class Result(COMPR_TLV_IE, tag=0x03):
|
||||||
GeneralResult = Enum(Int8ub,
|
GeneralResult = Enum(Int8ub,
|
||||||
# '0X' and '1X' indicate that the command has been performed
|
# '0X' and '1X' indicate that the command has been performed
|
||||||
performed_successfully=0,
|
performed_successfully=0,
|
||||||
@@ -255,27 +240,24 @@ class Result(COMPR_TLV_IE, tag=0x83):
|
|||||||
'launch_browser_generic_error': AddlInfoLaunchBrowser,
|
'launch_browser_generic_error': AddlInfoLaunchBrowser,
|
||||||
'bearer_independent_protocol_error': AddlInfoBip,
|
'bearer_independent_protocol_error': AddlInfoBip,
|
||||||
'frames_error': AddlInfoFrames
|
'frames_error': AddlInfoFrames
|
||||||
}, default=GreedyBytes))
|
}, default=HexAdapter(GreedyBytes)))
|
||||||
|
|
||||||
# TS 102 223 Section 8.13 + TS 31.111 Section 8.13
|
# TS 102 223 Section 8.13 + TS 31.111 Section 8.13
|
||||||
class SMS_TPDU(COMPR_TLV_IE, tag=0x8B):
|
class SMS_TPDU(COMPR_TLV_IE, tag=0x8B):
|
||||||
_construct = Struct('tpdu'/GreedyBytes)
|
_construct = Struct('tpdu'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 31.111 Section 8.14
|
# TS 31.111 Section 8.14
|
||||||
class SsString(COMPR_TLV_IE, tag=0x89):
|
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
|
# TS 102 223 Section 8.15
|
||||||
class TextString(COMPR_TLV_IE, tag=0x8D):
|
class TextString(COMPR_TLV_IE, tag=0x0d):
|
||||||
_test_de_encode = [
|
|
||||||
( '8d090470617373776f7264', {'dcs': 4, 'text_string': b'password'} )
|
|
||||||
]
|
|
||||||
_construct = Struct('dcs'/Int8ub, # TS 03.38
|
_construct = Struct('dcs'/Int8ub, # TS 03.38
|
||||||
'text_string'/GreedyBytes)
|
'text_string'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.16
|
# 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,
|
_construct = Struct('tone'/Enum(Int8ub, dial_tone=0x01,
|
||||||
called_subscriber_busy=0x02,
|
called_subscriber_busy=0x02,
|
||||||
congestion=0x03,
|
congestion=0x03,
|
||||||
@@ -306,39 +288,39 @@ class Tone(COMPR_TLV_IE, tag=0x8E):
|
|||||||
melody_8=0x47))
|
melody_8=0x47))
|
||||||
|
|
||||||
# TS 31 111 Section 8.17
|
# TS 31 111 Section 8.17
|
||||||
class USSDString(COMPR_TLV_IE, tag=0x8A):
|
class USSDString(COMPR_TLV_IE, tag=0x0a):
|
||||||
_construct = Struct('dcs'/Int8ub,
|
_construct = Struct('dcs'/Int8ub,
|
||||||
'ussd_string'/GreedyBytes)
|
'ussd_string'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.18
|
# TS 102 223 Section 8.18
|
||||||
class FileList(COMPR_TLV_IE, tag=0x92):
|
class FileList(COMPR_TLV_IE, tag=0x12):
|
||||||
FileId=Bytes(2)
|
FileId=HexAdapter(Bytes(2))
|
||||||
_construct = Struct('number_of_files'/Int8ub,
|
_construct = Struct('number_of_files'/Int8ub,
|
||||||
'files'/GreedyRange(FileId))
|
'files'/GreedyRange(FileId))
|
||||||
|
|
||||||
# TS 102 223 Section 8.19
|
# TS 102 223 Secton 8.19
|
||||||
class LocationInformation(COMPR_TLV_IE, tag=0x93):
|
class LocationInformation(COMPR_TLV_IE, tag=0x93):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 8.20
|
# TS 102 223 Secton 8.20
|
||||||
class IMEI(COMPR_TLV_IE, tag=0x94):
|
class IMEI(COMPR_TLV_IE, tag=0x94):
|
||||||
_construct = BcdAdapter(GreedyBytes)
|
_construct = BcdAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.21
|
# TS 102 223 Secton 8.21
|
||||||
class HelpRequest(COMPR_TLV_IE, tag=0x95):
|
class HelpRequest(COMPR_TLV_IE, tag=0x95):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 8.22
|
# TS 102 223 Secton 8.22
|
||||||
class NetworkMeasurementResults(COMPR_TLV_IE, tag=0x96):
|
class NetworkMeasurementResults(COMPR_TLV_IE, tag=0x96):
|
||||||
_construct = BcdAdapter(GreedyBytes)
|
_construct = BcdAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.23
|
# TS 102 223 Section 8.23
|
||||||
class DefaultText(COMPR_TLV_IE, tag=0x97):
|
class DefaultText(COMPR_TLV_IE, tag=0x97):
|
||||||
_construct = Struct('dcs'/Int8ub,
|
_construct = Struct('dcs'/Int8ub,
|
||||||
'text_string'/GreedyBytes)
|
'text_string'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.24
|
# TS 102 223 Section 8.24
|
||||||
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x98):
|
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x18):
|
||||||
_construct = GreedyRange(Int8ub)
|
_construct = GreedyRange(Int8ub)
|
||||||
|
|
||||||
class EventList(COMPR_TLV_IE, tag=0x99):
|
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)
|
_construct = Enum(Int8ub, normal_service=0, limited_service=1, no_service=2)
|
||||||
|
|
||||||
# TS 102 223 Section 8.31
|
# 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),
|
_construct = Struct('icon_qualifier'/FlagsEnum(Int8ub, not_self_explanatory=1),
|
||||||
'icon_identifier'/Int8ub)
|
'icon_identifier'/Int8ub)
|
||||||
|
|
||||||
@@ -394,7 +376,7 @@ class ItemIconIdentifierList(COMPR_TLV_IE, tag=0x9f):
|
|||||||
|
|
||||||
# TS 102 223 Section 8.35
|
# TS 102 223 Section 8.35
|
||||||
class CApdu(COMPR_TLV_IE, tag=0xA2):
|
class CApdu(COMPR_TLV_IE, tag=0xA2):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.37
|
# TS 102 223 Section 8.37
|
||||||
class TimerIdentifier(COMPR_TLV_IE, tag=0xA4):
|
class TimerIdentifier(COMPR_TLV_IE, tag=0xA4):
|
||||||
@@ -406,51 +388,28 @@ class TimerValue(COMPR_TLV_IE, tag=0xA5):
|
|||||||
|
|
||||||
# TS 102 223 Section 8.40
|
# TS 102 223 Section 8.40
|
||||||
class AtCommand(COMPR_TLV_IE, tag=0xA8):
|
class AtCommand(COMPR_TLV_IE, tag=0xA8):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.43
|
# TS 102 223 Section 8.43
|
||||||
class ImmediateResponse(COMPR_TLV_IE, tag=0xAB):
|
class ImmediateResponse(COMPR_TLV_IE, tag=0x2b):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 8.44
|
|
||||||
class DtmfString(COMPR_TLV_IE, tag=0xAC):
|
|
||||||
_construct = BcdAdapter(GreedyBytes)
|
|
||||||
|
|
||||||
# TS 102 223 Section 8.45
|
# TS 102 223 Section 8.45
|
||||||
class Language(COMPR_TLV_IE, tag=0xAD):
|
class Language(COMPR_TLV_IE, tag=0xAD):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 31.111 Section 8.46
|
# 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),
|
_construct = Struct('me_status'/Enum(Int8ub, in_idle_state=0, not_in_idle_state=1),
|
||||||
'timing_advance'/Int8ub)
|
'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
|
# TS 31.111 Section 8.49
|
||||||
class Bearer(COMPR_TLV_IE, tag=0xB2):
|
class Bearer(COMPR_TLV_IE, tag=0xB2):
|
||||||
SingleBearer = Enum(Int8ub, sms=0, csd=1, ussd=2, packet_Service=3)
|
SingleBearer = Enum(Int8ub, sms=0, csd=1, ussd=2, packet_Service=3)
|
||||||
_construct = GreedyRange(SingleBearer)
|
_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
|
# TS 102 223 Section 8.52
|
||||||
class BearerDescription(COMPR_TLV_IE, tag=0xB5):
|
class BearerDescription(COMPR_TLV_IE, tag=0xB5):
|
||||||
_test_de_encode = [
|
|
||||||
( 'b50103', {'bearer_parameters': b'', 'bearer_type': 'default'} ),
|
|
||||||
]
|
|
||||||
# TS 31.111 Section 8.52.1
|
# TS 31.111 Section 8.52.1
|
||||||
BearerParsCs = Struct('data_rate'/Int8ub,
|
BearerParsCs = Struct('data_rate'/Int8ub,
|
||||||
'bearer_service'/Int8ub,
|
'bearer_service'/Int8ub,
|
||||||
@@ -492,11 +451,11 @@ class BearerDescription(COMPR_TLV_IE, tag=0xB5):
|
|||||||
'packet_grps_utran_eutran': BearerParsPacket,
|
'packet_grps_utran_eutran': BearerParsPacket,
|
||||||
'packet_with_extd_params': BearerParsPacketExt,
|
'packet_with_extd_params': BearerParsPacketExt,
|
||||||
'ng_ran': BearerParsNgRan,
|
'ng_ran': BearerParsNgRan,
|
||||||
}, default=GreedyBytes))
|
}, default=HexAdapter(GreedyBytes)))
|
||||||
|
|
||||||
# TS 102 223 Section 8.53
|
# TS 102 223 Section 8.53
|
||||||
class ChannelData(COMPR_TLV_IE, tag = 0xB6):
|
class ChannelData(COMPR_TLV_IE, tag = 0xB6):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.54
|
# TS 102 223 Section 8.54
|
||||||
class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
|
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):
|
class BufferSize(COMPR_TLV_IE, tag = 0xB9):
|
||||||
_construct = Int16ub
|
_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):
|
class ChannelStatus(COMPR_TLV_IE, tag = 0xB8):
|
||||||
# complex decoding, depends on out-of-band context/knowledge :(
|
# 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
|
pass
|
||||||
_construct = GreedyBytes
|
|
||||||
|
|
||||||
# TS 102 223 Section 8.58
|
# TS 102 223 Section 8.58
|
||||||
class OtherAddress(COMPR_TLV_IE, tag = 0xBE):
|
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),
|
_construct = Struct('type_of_address'/Enum(Int8ub, ipv4=0x21, ipv6=0x57),
|
||||||
'address'/GreedyBytes)
|
'address'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.59
|
# TS 102 223 Section 8.59
|
||||||
class UiccTransportLevel(COMPR_TLV_IE, tag = 0xBC):
|
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,
|
_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_server=3, udp_uicc_client_local=4,
|
||||||
tcp_uicc_client_local=5, direct_channel=6),
|
tcp_uicc_client_local=5, direct_channel=6),
|
||||||
'port_number'/Int16ub)
|
'port_number'/Int16ub)
|
||||||
|
|
||||||
# TS 102 223 Section 8.60
|
# TS 102 223 Section 8.60
|
||||||
class Aid(COMPR_TLV_IE, tag=0xAF):
|
class Aid(COMPR_TLV_IE, tag=0x2f):
|
||||||
_construct = Struct('aid'/GreedyBytes)
|
_construct = Struct('aid'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.61
|
# TS 102 223 Section 8.61
|
||||||
class AccessTechnology(COMPR_TLV_IE, tag=0xBF):
|
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)
|
BearerTechId = Enum(Int8ub, technology_independent=0, bluetooth=1, irda=2, rs232=3, usb=4)
|
||||||
_construct = Struct('local_bearer_technology'/BearerTechId,
|
_construct = Struct('local_bearer_technology'/BearerTechId,
|
||||||
'service_identifier'/Int8ub,
|
'service_identifier'/Int8ub,
|
||||||
'service_record'/GreedyBytes)
|
'service_record'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.64
|
# TS 102 223 Section 8.64
|
||||||
class DeviceFilter(COMPR_TLV_IE, tag=0xC2):
|
class DeviceFilter(COMPR_TLV_IE, tag=0xC2):
|
||||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||||
'device_filter'/GreedyBytes)
|
'device_filter'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.65
|
# TS 102 223 Section 8.65
|
||||||
class ServiceSearchIE(COMPR_TLV_IE, tag=0xC3):
|
class ServiceSearchIE(COMPR_TLV_IE, tag=0xC3):
|
||||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||||
'service_search'/GreedyBytes)
|
'service_search'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.66
|
# TS 102 223 Section 8.66
|
||||||
class AttributeInformation(COMPR_TLV_IE, tag=0xC4):
|
class AttributeInformation(COMPR_TLV_IE, tag=0xC4):
|
||||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||||
'attribute_information'/GreedyBytes)
|
'attribute_information'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
|
|
||||||
# TS 102 223 Section 8.68
|
# TS 102 223 Section 8.68
|
||||||
class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
|
class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
|
||||||
_construct = Struct('coding_type'/Enum(Int8ub, ieee802_16=0, irda=1),
|
_construct = Struct('coding_type'/Enum(Int8ub, ieee802_16=0, irda=1),
|
||||||
'address'/GreedyBytes)
|
'address'/HexAdapter(GreedyBytes))
|
||||||
|
|
||||||
# TS 102 223 Section 8.70
|
# TS 102 223 Section 8.70
|
||||||
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
|
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
|
||||||
_test_de_encode = [
|
_construct = HexAdapter(GreedyBytes)
|
||||||
( 'c704036e6161', h2b('036e6161') ),
|
|
||||||
]
|
|
||||||
_construct = GreedyBytes
|
|
||||||
|
|
||||||
# TS 102 223 Section 8.72
|
# TS 102 223 Section 8.72
|
||||||
class TextAttribute(COMPR_TLV_IE, tag=0xD0):
|
class TextAttribute(COMPR_TLV_IE, tag=0x50):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 31.111 Section 8.72
|
# TS 31.111 Section 8.72
|
||||||
@@ -613,20 +562,20 @@ class ItemTextAttributeList(COMPR_TLV_IE, tag=0xD1):
|
|||||||
_construct = GreedyRange(Int8ub)
|
_construct = GreedyRange(Int8ub)
|
||||||
|
|
||||||
# TS 102 223 Section 8.80
|
# TS 102 223 Section 8.80
|
||||||
class FrameIdentifier(COMPR_TLV_IE, tag=0xE8):
|
class FrameIdentifier(COMPR_TLV_IE, tag=0x68):
|
||||||
_construct = Struct('identifier'/Int8ub)
|
_construct = Struct('identifier'/Int8ub)
|
||||||
|
|
||||||
# TS 102 223 Section 8.82
|
# TS 102 223 Section 8.82
|
||||||
class MultimediaMessageReference(COMPR_TLV_IE, tag=0xEA):
|
class MultimediaMessageReference(COMPR_TLV_IE, tag=0xEA):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.83
|
# TS 102 223 Section 8.83
|
||||||
class MultimediaMessageIdentifier(COMPR_TLV_IE, tag=0xEB):
|
class MultimediaMessageIdentifier(COMPR_TLV_IE, tag=0xEB):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.85
|
# TS 102 223 Section 8.85
|
||||||
class MmContentIdentifier(COMPR_TLV_IE, tag=0xEE):
|
class MmContentIdentifier(COMPR_TLV_IE, tag=0xEE):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.89
|
# TS 102 223 Section 8.89
|
||||||
class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
|
class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
|
||||||
@@ -634,11 +583,11 @@ class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
|
|||||||
|
|
||||||
# TS 31.111 Section 8.90
|
# TS 31.111 Section 8.90
|
||||||
class PlmnWactList(COMPR_TLV_IE, tag=0xF2):
|
class PlmnWactList(COMPR_TLV_IE, tag=0xF2):
|
||||||
def _from_bytes(self, do: bytes):
|
def _from_bytes(self, x):
|
||||||
r = []
|
r = []
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(do):
|
while i < len(x):
|
||||||
r.append(dec_xplmn_w_act(b2h(do[i:i+5])))
|
r.append(dec_xplmn_w_act(b2h(x[i:i+5])))
|
||||||
i += 5
|
i += 5
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@@ -649,7 +598,7 @@ class ContactlessFunctionalityState(COMPR_TLV_IE, tag=0xD4):
|
|||||||
# TS 31.111 Section 8.91
|
# TS 31.111 Section 8.91
|
||||||
class RoutingAreaIdentification(COMPR_TLV_IE, tag=0xF3):
|
class RoutingAreaIdentification(COMPR_TLV_IE, tag=0xF3):
|
||||||
_construct = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
|
_construct = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
|
||||||
'lac'/Bytes(2),
|
'lac'/HexAdapter(Bytes(2)),
|
||||||
'rac'/Int8ub)
|
'rac'/Int8ub)
|
||||||
|
|
||||||
# TS 31.111 Section 8.92
|
# TS 31.111 Section 8.92
|
||||||
@@ -709,23 +658,23 @@ class EcatSequenceNumber(COMPR_TLV_IE, tag=0xA1):
|
|||||||
|
|
||||||
# TS 102 223 Section 8.99
|
# TS 102 223 Section 8.99
|
||||||
class EncryptedTlvList(COMPR_TLV_IE, tag=0xA2):
|
class EncryptedTlvList(COMPR_TLV_IE, tag=0xA2):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.100
|
# TS 102 223 Section 8.100
|
||||||
class Mac(COMPR_TLV_IE, tag=0xE0):
|
class Mac(COMPR_TLV_IE, tag=0xE0):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.101
|
# TS 102 223 Section 8.101
|
||||||
class SaTemplate(COMPR_TLV_IE, tag=0xA3):
|
class SaTemplate(COMPR_TLV_IE, tag=0xA3):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.103
|
# 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)
|
_construct = FlagsEnum(Byte, even_if_navigating_menus=0, even_if_data_call=1, even_if_voice_call=2)
|
||||||
|
|
||||||
# TS 102 223 Section 8.104
|
# TS 102 223 Section 8.104
|
||||||
class DnsServerAddress(COMPR_TLV_IE, tag=0xC0):
|
class DnsServerAddress(COMPR_TLV_IE, tag=0xC0):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
|
||||||
# TS 102 223 Section 8.105
|
# TS 102 223 Section 8.105
|
||||||
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
||||||
@@ -734,7 +683,7 @@ class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
|||||||
_construct = GreedyRange(AccessTechTuple)
|
_construct = GreedyRange(AccessTechTuple)
|
||||||
|
|
||||||
# TS 102 223 Section 8.107
|
# TS 102 223 Section 8.107
|
||||||
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0xBB):
|
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0x3B):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 31.111 Section 8.108
|
# TS 31.111 Section 8.108
|
||||||
@@ -768,194 +717,19 @@ class SMSCBDownload(BER_TLV_IE, tag=0xD2,
|
|||||||
nested=[DeviceIdentities, CBSPage]):
|
nested=[DeviceIdentities, CBSPage]):
|
||||||
pass
|
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,
|
class USSDDownload(BER_TLV_IE, tag=0xD9,
|
||||||
nested=[DeviceIdentities, USSDString]):
|
nested=[DeviceIdentities, USSDString]):
|
||||||
pass
|
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):
|
class ProactiveCmd(BER_TLV_IE):
|
||||||
def _compute_tag(self) -> int:
|
def _compute_tag(self) -> int:
|
||||||
return 0xD0
|
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
|
# TS 101 220 Table 7.17 + 102 223 6.6.13/9.4 + TS 31.111 6.6.13
|
||||||
class Refresh(ProactiveCmd, tag=0x01,
|
class Refresh(ProactiveCmd, tag=0x01,
|
||||||
nested=[CommandDetails, DeviceIdentities, FileList, Aid, AlphaIdentifier,
|
nested=[CommandDetails, DeviceIdentities, FileList, Aid, AlphaIdentifier,
|
||||||
@@ -963,24 +737,20 @@ class Refresh(ProactiveCmd, tag=0x01,
|
|||||||
ApplicationSpecificRefreshData, PlmnWactList, PlmnList]):
|
ApplicationSpecificRefreshData, PlmnWactList, PlmnList]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 6.6.4
|
|
||||||
class MoreTime(ProactiveCmd, tag=0x02,
|
class MoreTime(ProactiveCmd, tag=0x02,
|
||||||
nested=[CommandDetails, DeviceIdentities]):
|
nested=[CommandDetails]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 6.6.5
|
|
||||||
class PollInterval(ProactiveCmd, tag=0x03,
|
class PollInterval(ProactiveCmd, tag=0x03,
|
||||||
nested=[CommandDetails, DeviceIdentities, Duration]):
|
nested=[CommandDetails]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 6.6.14
|
|
||||||
class PollingOff(ProactiveCmd, tag=0x04,
|
class PollingOff(ProactiveCmd, tag=0x04,
|
||||||
nested=[CommandDetails, DeviceIdentities]):
|
nested=[CommandDetails]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 Section 6.6.16
|
|
||||||
class SetUpEventList(ProactiveCmd, tag=0x05,
|
class SetUpEventList(ProactiveCmd, tag=0x05,
|
||||||
nested=[CommandDetails, DeviceIdentities, EventList]):
|
nested=[CommandDetails]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 31.111 Section 6.6.12
|
# TS 31.111 Section 6.6.12
|
||||||
@@ -1008,27 +778,20 @@ class SendShortMessage(ProactiveCmd, tag=0x13,
|
|||||||
SMS_TPDU, IconIdentifier, TextAttribute, FrameIdentifier]):
|
SMS_TPDU, IconIdentifier, TextAttribute, FrameIdentifier]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 6.6.24
|
|
||||||
class SendDTMF(ProactiveCmd, tag=0x14,
|
class SendDTMF(ProactiveCmd, tag=0x14,
|
||||||
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
|
nested=[CommandDetails]):
|
||||||
DtmfString, IconIdentifier, TextAttribute, FrameIdentifier]):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 6.6.26
|
|
||||||
class LaunchBrowser(ProactiveCmd, tag=0x15,
|
class LaunchBrowser(ProactiveCmd, tag=0x15,
|
||||||
nested=[CommandDetails, DeviceIdentities, BrowserIdentity, Url, Bearer, ProvisioningFileReference,
|
nested=[CommandDetails]):
|
||||||
TextString, AlphaIdentifier, IconIdentifier, TextAttribute, FrameIdentifier,
|
|
||||||
NetworkAccessName]):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class GeographicalLocationRequest(ProactiveCmd, tag=0x16,
|
class GeographicalLocationRequest(ProactiveCmd, tag=0x16,
|
||||||
nested=[CommandDetails]):
|
nested=[CommandDetails]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 102 223 6.6.5
|
|
||||||
class PlayTone(ProactiveCmd, tag=0x20,
|
class PlayTone(ProactiveCmd, tag=0x20,
|
||||||
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
|
nested=[CommandDetails]):
|
||||||
Tone, Duration, IconIdentifier, TextAttribute, FrameIdentifier]):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TS 101 220 Table 7.17 + 102 223 6.6.1/9.4 CMD=0x21
|
# 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:
|
for c in self.children:
|
||||||
if type(c).__name__ == 'CommandDetails':
|
if type(c).__name__ == 'CommandDetails':
|
||||||
return c
|
return c
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
class ProactiveCommand(TLV_IE_Collection,
|
class ProactiveCommand(TLV_IE_Collection,
|
||||||
nested=[Refresh, MoreTime, PollInterval, PollingOff, SetUpEventList, SetUpCall,
|
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
|
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
|
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."""
|
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
|
# do a first parse step to get the CommandDetails
|
||||||
pcmd = ProactiveCommandBase()
|
pcmd = ProactiveCommandBase()
|
||||||
pcmd.from_tlv(binary)
|
pcmd.from_tlv(binary)
|
||||||
cmd_details = pcmd.find_cmd_details()
|
cmd_details = pcmd.find_cmd_details()
|
||||||
# then do a second decode stage for the specific
|
# 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:
|
if cmd_type in self.members_by_tag:
|
||||||
cls = self.members_by_tag[cmd_type]
|
cls = self.members_by_tag[cmd_type]
|
||||||
inst = cls()
|
inst = cls()
|
||||||
_dec, remainder = inst.from_tlv(binary)
|
dec, remainder = inst.from_tlv(binary)
|
||||||
self.decoded = inst
|
self.decoded = inst
|
||||||
else:
|
else:
|
||||||
self.decoded = pcmd
|
self.decoded = pcmd
|
||||||
@@ -1256,18 +1019,9 @@ class ProactiveCommand(TLV_IE_Collection,
|
|||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return self.decoded.to_dict()
|
return self.decoded.to_dict()
|
||||||
|
|
||||||
def to_bytes(self, context: dict = {}):
|
def to_bytes(self):
|
||||||
return self.decoded.to_tlv()
|
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
|
# reasonable default for playing with OTA
|
||||||
# 010203040506070809101112131415161718192021222324252627282930313233
|
# 010203040506070809101112131415161718192021222324252627282930313233
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
from construct import Bytewise, BitStruct, BitsInteger, Struct, FlagsEnum
|
from pySim.utils import *
|
||||||
from osmocom.utils import *
|
|
||||||
from osmocom.construct import *
|
|
||||||
|
|
||||||
from pySim.filesystem import *
|
from pySim.filesystem import *
|
||||||
|
from pySim.profile import match_ruim
|
||||||
from pySim.profile import CardProfile, CardProfileAddon
|
from pySim.profile import CardProfile, CardProfileAddon
|
||||||
from pySim.ts_51_011 import CardProfileSIM
|
from pySim.ts_51_011 import CardProfileSIM
|
||||||
from pySim.ts_51_011 import DF_TELECOM, DF_GSM
|
from pySim.ts_51_011 import DF_TELECOM, DF_GSM
|
||||||
from pySim.ts_51_011 import EF_ServiceTable
|
from pySim.ts_51_011 import EF_ServiceTable
|
||||||
|
from pySim.construct import *
|
||||||
|
from construct import *
|
||||||
|
|
||||||
|
|
||||||
# Mapping between CDMA Service Number and its description
|
# Mapping between CDMA Service Number and its description
|
||||||
@@ -115,7 +115,7 @@ class EF_AD(TransparentEF):
|
|||||||
'''3.4.33 Administrative Data'''
|
'''3.4.33 Administrative Data'''
|
||||||
|
|
||||||
_test_de_encode = [
|
_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
|
_test_no_pad = True
|
||||||
|
|
||||||
@@ -134,9 +134,9 @@ class EF_AD(TransparentEF):
|
|||||||
# Byte 1: Display Condition
|
# Byte 1: Display Condition
|
||||||
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
|
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
|
||||||
# Bytes 2-3: Additional information
|
# Bytes 2-3: Additional information
|
||||||
'additional_info'/Bytes(2),
|
'additional_info'/HexAdapter(Bytes(2)),
|
||||||
# Bytes 4..: RFU
|
# Bytes 4..: RFU
|
||||||
'rfu'/GreedyBytesRFU,
|
'rfu'/HexAdapter(GreedyBytesRFU),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,23 +178,20 @@ class DF_CDMA(CardDF):
|
|||||||
class CardProfileRUIM(CardProfile):
|
class CardProfileRUIM(CardProfile):
|
||||||
'''R-UIM card profile as per 3GPP2 C.S0023-D'''
|
'''R-UIM card profile as per 3GPP2 C.S0023-D'''
|
||||||
|
|
||||||
ORDER = 20
|
ORDER = 2
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('R-UIM', desc='CDMA R-UIM Card', cla="a0",
|
super().__init__('R-UIM', desc='CDMA R-UIM Card', cla="a0",
|
||||||
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
|
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
|
||||||
|
|
||||||
@staticmethod
|
@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)
|
# TODO: Response parameters/data in case of DF_CDMA (section 2.6)
|
||||||
return CardProfileSIM.decode_select_response(data_hex)
|
return CardProfileSIM.decode_select_response(resp_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"])
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def match_with_card(scc: SimCardCommands) -> bool:
|
||||||
|
return match_ruim(scc)
|
||||||
|
|
||||||
class AddonRUIM(CardProfileAddon):
|
class AddonRUIM(CardProfileAddon):
|
||||||
"""An Addon that can be found on on a combined SIM + RUIM or UICC + RUIM to support CDMA."""
|
"""An Addon that can be found on on a combined SIM + RUIM or UICC + RUIM to support CDMA."""
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
|
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
|
||||||
# Copyright (C) 2010-2024 Harald Welte <laforge@gnumonks.org>
|
# Copyright (C) 2010-2023 Harald Welte <laforge@gnumonks.org>
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -21,15 +21,13 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# 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
|
import typing # construct also has a Union, so we do typing.Union below
|
||||||
from construct import Construct, Struct, Const, Select
|
|
||||||
from construct import Optional as COptional
|
|
||||||
from osmocom.construct import LV, filter_dict
|
|
||||||
from osmocom.utils import rpad, lpad, b2h, h2b, h2i, i2h, str_sanitize, Hexstr
|
|
||||||
from osmocom.tlv import bertlv_encode_len
|
|
||||||
|
|
||||||
from pySim.utils import sw_match, expand_hex, SwHexstr, ResTuple, SwMatchstr
|
from construct import *
|
||||||
|
from pySim.construct import LV
|
||||||
|
from pySim.utils import rpad, lpad, b2h, h2b, sw_match, bertlv_encode_len, Hexstr, h2i, i2h, str_sanitize, expand_hex
|
||||||
|
from pySim.utils import Hexstr, SwHexstr, ResTuple
|
||||||
from pySim.exceptions import SwMatchError
|
from pySim.exceptions import SwMatchError
|
||||||
from pySim.transport import LinkBase
|
from pySim.transport import LinkBase
|
||||||
|
|
||||||
@@ -66,115 +64,41 @@ class SimCardCommands:
|
|||||||
byte by the respective instance. """
|
byte by the respective instance. """
|
||||||
def __init__(self, transport: LinkBase, lchan_nr: int = 0):
|
def __init__(self, transport: LinkBase, lchan_nr: int = 0):
|
||||||
self._tp = transport
|
self._tp = transport
|
||||||
|
self._cla_byte = None
|
||||||
self.sel_ctrl = "0000"
|
self.sel_ctrl = "0000"
|
||||||
self.lchan_nr = lchan_nr
|
self.lchan_nr = lchan_nr
|
||||||
# invokes the setter below
|
# invokes the setter below
|
||||||
self.cla_byte = "a0"
|
self.cla_byte = "a0"
|
||||||
self.scp = None # Secure Channel Protocol
|
|
||||||
|
|
||||||
def fork_lchan(self, lchan_nr: int) -> 'SimCardCommands':
|
def fork_lchan(self, lchan_nr: int) -> 'SimCardCommands':
|
||||||
"""Fork a per-lchan specific SimCardCommands instance off the current instance."""
|
"""Fork a per-lchan specific SimCardCommands instance off the current instance."""
|
||||||
ret = SimCardCommands(transport = self._tp, lchan_nr = lchan_nr)
|
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
|
ret.sel_ctrl = self.sel_ctrl
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_cmd_len(self) -> int:
|
def cla_byte(self) -> Hexstr:
|
||||||
"""Maximum length of the command apdu data section. Depends on secure channel protocol used."""
|
"""Return the (cached) patched default CLA byte for this card."""
|
||||||
if self.scp:
|
return self._cla4lchan
|
||||||
return 255 - self.scp.overhead
|
|
||||||
|
@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:
|
else:
|
||||||
return 255
|
return cla_with_lchan(cla, self.lchan_nr)
|
||||||
|
|
||||||
def send_apdu(self, pdu: Hexstr, apply_lchan:bool = True) -> ResTuple:
|
|
||||||
"""Sends an APDU and auto fetch response data
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
|
||||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
|
||||||
Returns:
|
|
||||||
tuple(data, sw), where
|
|
||||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
|
||||||
sw : string (in hex) of status word (ex. "9000")
|
|
||||||
"""
|
|
||||||
if apply_lchan:
|
|
||||||
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
|
|
||||||
if self.scp:
|
|
||||||
return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
|
|
||||||
else:
|
|
||||||
return self._tp.send_apdu(pdu)
|
|
||||||
|
|
||||||
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000", apply_lchan:bool = True) -> ResTuple:
|
|
||||||
"""Sends an APDU and check returned SW
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
|
||||||
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
|
|
||||||
digits using a '?' to add some ambiguity if needed.
|
|
||||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
|
||||||
Returns:
|
|
||||||
tuple(data, sw), where
|
|
||||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
|
||||||
sw : string (in hex) of status word (ex. "9000")
|
|
||||||
"""
|
|
||||||
if apply_lchan:
|
|
||||||
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
|
|
||||||
if self.scp:
|
|
||||||
return self.scp.send_apdu_wrapper(self._tp.send_apdu_checksw, pdu, sw)
|
|
||||||
else:
|
|
||||||
return self._tp.send_apdu_checksw(pdu, sw)
|
|
||||||
|
|
||||||
def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr, cmd_constr: Construct,
|
|
||||||
cmd_data: Hexstr, resp_constr: Construct, apply_lchan:bool = True) -> Tuple[dict, SwHexstr]:
|
|
||||||
"""Build and sends an APDU using a 'construct' definition; parses response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cla : string (in hex) ISO 7816 class byte
|
|
||||||
ins : string (in hex) ISO 7816 instruction byte
|
|
||||||
p1 : string (in hex) ISO 7116 Parameter 1 byte
|
|
||||||
p2 : string (in hex) ISO 7116 Parameter 2 byte
|
|
||||||
cmd_cosntr : defining how to generate binary APDU command data
|
|
||||||
cmd_data : command data passed to cmd_constr
|
|
||||||
resp_cosntr : defining how to decode binary APDU response data
|
|
||||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
|
||||||
Returns:
|
|
||||||
Tuple of (decoded_data, sw)
|
|
||||||
"""
|
|
||||||
cmd = cmd_constr.build(cmd_data) if cmd_data else 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)
|
|
||||||
if data:
|
|
||||||
# filter the resulting dict to avoid '_io' members inside
|
|
||||||
rsp = filter_dict(resp_constr.parse(h2b(data)))
|
|
||||||
else:
|
|
||||||
rsp = None
|
|
||||||
return (rsp, sw)
|
|
||||||
|
|
||||||
def send_apdu_constr_checksw(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr,
|
|
||||||
cmd_constr: Construct, cmd_data: Hexstr, resp_constr: Construct,
|
|
||||||
sw_exp: SwMatchstr="9000", apply_lchan:bool = True) -> Tuple[dict, SwHexstr]:
|
|
||||||
"""Build and sends an APDU using a 'construct' definition; parses response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cla : string (in hex) ISO 7816 class byte
|
|
||||||
ins : string (in hex) ISO 7816 instruction byte
|
|
||||||
p1 : string (in hex) ISO 7116 Parameter 1 byte
|
|
||||||
p2 : string (in hex) ISO 7116 Parameter 2 byte
|
|
||||||
cmd_cosntr : defining how to generate binary APDU command data
|
|
||||||
cmd_data : command data passed to cmd_constr
|
|
||||||
resp_cosntr : defining how to decode binary APDU response data
|
|
||||||
exp_sw : string (in hex) of status word (ex. "9000")
|
|
||||||
Returns:
|
|
||||||
Tuple of (decoded_data, sw)
|
|
||||||
"""
|
|
||||||
(rsp, sw) = self.send_apdu_constr(cla, ins, p1, p2, cmd_constr, cmd_data, resp_constr,
|
|
||||||
apply_lchan = apply_lchan)
|
|
||||||
if not sw_match(sw, sw_exp):
|
|
||||||
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
|
|
||||||
return (rsp, sw)
|
|
||||||
|
|
||||||
# Extract a single FCP item from TLV
|
# Extract a single FCP item from TLV
|
||||||
def __parse_fcp(self, fcp: Hexstr):
|
def __parse_fcp(self, fcp: Hexstr):
|
||||||
@@ -196,7 +120,6 @@ class SimCardCommands:
|
|||||||
# checking if the length of the remaining TLV string matches
|
# checking if the length of the remaining TLV string matches
|
||||||
# what we get in the length field.
|
# what we get in the length field.
|
||||||
# See also ETSI TS 102 221, chapter 11.1.1.3.0 Base coding.
|
# 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)
|
exp_tlv_len = int(fcp[2:4], 16)
|
||||||
if len(fcp[4:]) // 2 == exp_tlv_len:
|
if len(fcp[4:]) // 2 == exp_tlv_len:
|
||||||
skip = 4
|
skip = 4
|
||||||
@@ -204,7 +127,6 @@ class SimCardCommands:
|
|||||||
exp_tlv_len = int(fcp[2:6], 16)
|
exp_tlv_len = int(fcp[2:6], 16)
|
||||||
if len(fcp[4:]) // 2 == exp_tlv_len:
|
if len(fcp[4:]) // 2 == exp_tlv_len:
|
||||||
skip = 6
|
skip = 6
|
||||||
raise ValueError('Cannot determine length of TLV-length')
|
|
||||||
|
|
||||||
# Skip FCP tag and length
|
# Skip FCP tag and length
|
||||||
tlv = fcp[skip:]
|
tlv = fcp[skip:]
|
||||||
@@ -245,10 +167,11 @@ class SimCardCommands:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
rv = []
|
rv = []
|
||||||
if not isinstance(dir_list, list):
|
if type(dir_list) is not list:
|
||||||
dir_list = [dir_list]
|
dir_list = [dir_list]
|
||||||
for i in dir_list:
|
for i in dir_list:
|
||||||
data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i + "00")
|
data, sw = self._tp.send_apdu(
|
||||||
|
self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
|
||||||
rv.append((data, sw))
|
rv.append((data, sw))
|
||||||
if sw != '9000':
|
if sw != '9000':
|
||||||
return rv
|
return rv
|
||||||
@@ -264,10 +187,10 @@ class SimCardCommands:
|
|||||||
list of return values (FCP in hex encoding) for each element of the path
|
list of return values (FCP in hex encoding) for each element of the path
|
||||||
"""
|
"""
|
||||||
rv = []
|
rv = []
|
||||||
if not isinstance(dir_list, list):
|
if type(dir_list) is not list:
|
||||||
dir_list = [dir_list]
|
dir_list = [dir_list]
|
||||||
for i in dir_list:
|
for i in dir_list:
|
||||||
data, _sw = self.select_file(i)
|
data, sw = self.select_file(i)
|
||||||
rv.append(data)
|
rv.append(data)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
@@ -278,21 +201,21 @@ class SimCardCommands:
|
|||||||
fid : file identifier as hex string
|
fid : file identifier as hex string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid + "00")
|
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
|
||||||
|
|
||||||
def select_parent_df(self) -> ResTuple:
|
def select_parent_df(self) -> ResTuple:
|
||||||
"""Execute SELECT to switch to the parent DF """
|
"""Execute SELECT to switch to the parent DF """
|
||||||
return self.send_apdu_checksw(self.cla_byte + "a40304")
|
return self._tp.send_apdu_checksw(self.cla_byte + "a4030400")
|
||||||
|
|
||||||
def select_adf(self, aid: Hexstr) -> ResTuple:
|
def select_adf(self, aid: Hexstr) -> ResTuple:
|
||||||
"""Execute SELECT a given Application ADF.
|
"""Execute SELECT a given Applicaiton ADF.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
aid : application identifier as hex string
|
aid : application identifier as hex string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
|
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
|
||||||
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid + "00")
|
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
|
||||||
|
|
||||||
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
|
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
|
||||||
"""Execute READD BINARY.
|
"""Execute READD BINARY.
|
||||||
@@ -313,14 +236,14 @@ class SimCardCommands:
|
|||||||
total_data = ''
|
total_data = ''
|
||||||
chunk_offset = 0
|
chunk_offset = 0
|
||||||
while chunk_offset < length:
|
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 + \
|
pdu = self.cla_byte + \
|
||||||
'b0%04x%02x' % (offset + chunk_offset, chunk_len)
|
'b0%04x%02x' % (offset + chunk_offset, chunk_len)
|
||||||
try:
|
try:
|
||||||
data, sw = self.send_apdu_checksw(pdu)
|
data, sw = self._tp.send_apdu_checksw(pdu)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
e.add_note('failed to read (offset %d)' % offset)
|
raise ValueError('%s, failed to read (offset %d)' %
|
||||||
raise e
|
(str_sanitize(str(e)), offset))
|
||||||
total_data += data
|
total_data += data
|
||||||
chunk_offset += chunk_len
|
chunk_offset += chunk_len
|
||||||
return total_data, sw
|
return total_data, sw
|
||||||
@@ -371,16 +294,16 @@ class SimCardCommands:
|
|||||||
total_data = ''
|
total_data = ''
|
||||||
chunk_offset = 0
|
chunk_offset = 0
|
||||||
while chunk_offset < data_length:
|
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
|
# chunk_offset is bytes, but data slicing is hex chars, so we need to multiply by 2
|
||||||
pdu = self.cla_byte + \
|
pdu = self.cla_byte + \
|
||||||
'd6%04x%02x' % (offset + chunk_offset, chunk_len) + \
|
'd6%04x%02x' % (offset + chunk_offset, chunk_len) + \
|
||||||
data[chunk_offset*2: (chunk_offset+chunk_len)*2]
|
data[chunk_offset*2: (chunk_offset+chunk_len)*2]
|
||||||
try:
|
try:
|
||||||
chunk_data, chunk_sw = self.send_apdu_checksw(pdu)
|
chunk_data, chunk_sw = self._tp.send_apdu_checksw(pdu)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
e.add_note('failed to write chunk (chunk_offset %d, chunk_len %d)' % (chunk_offset, chunk_len))
|
raise ValueError('%s, failed to write chunk (chunk_offset %d, chunk_len %d)' %
|
||||||
raise e
|
(str_sanitize(str(e)), chunk_offset, chunk_len))
|
||||||
total_data += data
|
total_data += data
|
||||||
chunk_offset += chunk_len
|
chunk_offset += chunk_len
|
||||||
if verify:
|
if verify:
|
||||||
@@ -397,7 +320,7 @@ class SimCardCommands:
|
|||||||
r = self.select_path(ef)
|
r = self.select_path(ef)
|
||||||
rec_length = self.__record_len(r)
|
rec_length = self.__record_len(r)
|
||||||
pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length)
|
pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length)
|
||||||
return self.send_apdu_checksw(pdu)
|
return self._tp.send_apdu_checksw(pdu)
|
||||||
|
|
||||||
def __verify_record(self, ef: Path, rec_no: int, data: str):
|
def __verify_record(self, ef: Path, rec_no: int, data: str):
|
||||||
"""Verify record against given data
|
"""Verify record against given data
|
||||||
@@ -436,10 +359,10 @@ class SimCardCommands:
|
|||||||
else:
|
else:
|
||||||
# make sure the input data is padded to the record length using 0xFF.
|
# make sure the input data is padded to the record length using 0xFF.
|
||||||
# In cases where the input data exceed we throw an exception.
|
# 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)' % (
|
raise ValueError('Data length exceeds record length (expected max %d, got %d)' % (
|
||||||
rec_length, len(data) // 2))
|
rec_length, len(data) // 2))
|
||||||
elif len(data) // 2 < rec_length:
|
elif (len(data) // 2 < rec_length):
|
||||||
if leftpad:
|
if leftpad:
|
||||||
data = lpad(data, rec_length * 2)
|
data = lpad(data, rec_length * 2)
|
||||||
else:
|
else:
|
||||||
@@ -460,7 +383,7 @@ class SimCardCommands:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
|
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
|
||||||
res = self.send_apdu_checksw(pdu)
|
res = self._tp.send_apdu_checksw(pdu)
|
||||||
if verify:
|
if verify:
|
||||||
self.__verify_record(ef, rec_no, data)
|
self.__verify_record(ef, rec_no, data)
|
||||||
return res
|
return res
|
||||||
@@ -495,10 +418,10 @@ class SimCardCommands:
|
|||||||
# TS 102 221 Section 11.3.1 low-level helper
|
# TS 102 221 Section 11.3.1 low-level helper
|
||||||
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
|
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
|
||||||
if first:
|
if first:
|
||||||
pdu = '80cb008001%02x00' % (tag)
|
pdu = self.cla4lchan('80') + 'cb008001%02x' % (tag)
|
||||||
else:
|
else:
|
||||||
pdu = '80cb0000'
|
pdu = self.cla4lchan('80') + 'cb000000'
|
||||||
return self.send_apdu_checksw(pdu)
|
return self._tp.send_apdu_checksw(pdu)
|
||||||
|
|
||||||
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
|
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
|
||||||
"""Execute RETRIEVE DATA, see also TS 102 221 Section 11.3.1.
|
"""Execute RETRIEVE DATA, see also TS 102 221 Section 11.3.1.
|
||||||
@@ -514,7 +437,7 @@ class SimCardCommands:
|
|||||||
# retrieve first block
|
# retrieve first block
|
||||||
data, sw = self._retrieve_data(tag, first=True)
|
data, sw = self._retrieve_data(tag, first=True)
|
||||||
total_data += data
|
total_data += data
|
||||||
while sw in ['62f1', '62f2']:
|
while sw == '62f1' or sw == '62f2':
|
||||||
data, sw = self._retrieve_data(tag, first=False)
|
data, sw = self._retrieve_data(tag, first=False)
|
||||||
total_data += data
|
total_data += data
|
||||||
return total_data, sw
|
return total_data, sw
|
||||||
@@ -525,10 +448,10 @@ class SimCardCommands:
|
|||||||
p1 = 0x80
|
p1 = 0x80
|
||||||
else:
|
else:
|
||||||
p1 = 0x00
|
p1 = 0x00
|
||||||
if isinstance(data, (bytes, bytearray)):
|
if isinstance(data, bytes) or isinstance(data, bytearray):
|
||||||
data = b2h(data)
|
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)
|
return self._tp.send_apdu_checksw(pdu)
|
||||||
|
|
||||||
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
|
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
|
||||||
"""Execute SET DATA.
|
"""Execute SET DATA.
|
||||||
@@ -555,10 +478,10 @@ class SimCardCommands:
|
|||||||
total_len = len(tlv_bin)
|
total_len = len(tlv_bin)
|
||||||
remaining = tlv_bin
|
remaining = tlv_bin
|
||||||
while len(remaining) > 0:
|
while len(remaining) > 0:
|
||||||
fragment = remaining[:self.max_cmd_len]
|
fragment = remaining[:255]
|
||||||
rdata, sw = self._set_data(fragment, first=first)
|
rdata, sw = self._set_data(fragment, first=first)
|
||||||
first = False
|
first = False
|
||||||
remaining = remaining[self.max_cmd_len:]
|
remaining = remaining[255:]
|
||||||
return rdata, sw
|
return rdata, sw
|
||||||
|
|
||||||
def run_gsm(self, rand: Hexstr) -> ResTuple:
|
def run_gsm(self, rand: Hexstr) -> ResTuple:
|
||||||
@@ -570,20 +493,21 @@ class SimCardCommands:
|
|||||||
if len(rand) != 32:
|
if len(rand) != 32:
|
||||||
raise ValueError('Invalid rand')
|
raise ValueError('Invalid rand')
|
||||||
self.select_path(['3f00', '7f20'])
|
self.select_path(['3f00', '7f20'])
|
||||||
return self.send_apdu_checksw('a088000010' + rand + '00', sw='9000')
|
return self._tp.send_apdu_checksw(self.cla4lchan('a0') + '88000010' + rand, sw='9000')
|
||||||
|
|
||||||
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
|
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
|
||||||
"""Execute AUTHENTICATE (USIM/ISIM).
|
"""Execute AUTHENTICATE (USIM/ISIM).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rand : 16 byte random data as hex string (RAND)
|
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')
|
context : 16 byte random data ('3g' or 'gsm')
|
||||||
"""
|
"""
|
||||||
# 3GPP TS 31.102 Section 7.1.2.1
|
# 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)
|
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)
|
AuthResp3G = Select(AuthResp3GSyncFail, AuthResp3GSuccess)
|
||||||
# build parameters
|
# build parameters
|
||||||
cmd_data = {'rand': rand, 'autn': autn}
|
cmd_data = {'rand': rand, 'autn': autn}
|
||||||
@@ -591,9 +515,7 @@ class SimCardCommands:
|
|||||||
p2 = '81'
|
p2 = '81'
|
||||||
elif context == 'gsm':
|
elif context == 'gsm':
|
||||||
p2 = '80'
|
p2 = '80'
|
||||||
else:
|
(data, sw) = self._tp.send_apdu_constr_checksw(
|
||||||
raise ValueError("Unsupported context '%s'" % context)
|
|
||||||
(data, sw) = self.send_apdu_constr_checksw(
|
|
||||||
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
|
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
|
||||||
if 'auts' in data:
|
if 'auts' in data:
|
||||||
ret = {'synchronisation_failure': data}
|
ret = {'synchronisation_failure': data}
|
||||||
@@ -603,11 +525,11 @@ class SimCardCommands:
|
|||||||
|
|
||||||
def status(self) -> ResTuple:
|
def status(self) -> ResTuple:
|
||||||
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
|
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
|
||||||
return self.send_apdu_checksw('80F20000')
|
return self._tp.send_apdu_checksw(self.cla4lchan('80') + 'F20000ff')
|
||||||
|
|
||||||
def deactivate_file(self) -> ResTuple:
|
def deactivate_file(self) -> ResTuple:
|
||||||
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
|
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
|
||||||
return self.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
|
return self._tp.send_apdu_constr_checksw(self.cla_byte, '04', '00', '00', None, None, None)
|
||||||
|
|
||||||
def activate_file(self, fid: Hexstr) -> ResTuple:
|
def activate_file(self, fid: Hexstr) -> ResTuple:
|
||||||
"""Execute ACTIVATE FILE command as per TS 102 221 Section 11.1.15.
|
"""Execute ACTIVATE FILE command as per TS 102 221 Section 11.1.15.
|
||||||
@@ -615,31 +537,31 @@ class SimCardCommands:
|
|||||||
Args:
|
Args:
|
||||||
fid : file identifier as hex string
|
fid : file identifier as hex string
|
||||||
"""
|
"""
|
||||||
return self.send_apdu_checksw(self.cla_byte + '44000002' + fid)
|
return self._tp.send_apdu_checksw(self.cla_byte + '44000002' + fid)
|
||||||
|
|
||||||
def create_file(self, payload: Hexstr) -> ResTuple:
|
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))
|
return self._tp.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
|
||||||
|
|
||||||
def resize_file(self, payload: Hexstr) -> ResTuple:
|
def resize_file(self, payload: Hexstr) -> ResTuple:
|
||||||
"""Execute RESIZE FILE command as per TS 102 222 Section 6.10"""
|
"""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._tp.send_apdu_checksw(self.cla4lchan('80') + 'd40000%02x%s' % (len(payload)//2, payload))
|
||||||
|
|
||||||
def delete_file(self, fid: Hexstr) -> ResTuple:
|
def delete_file(self, fid: Hexstr) -> ResTuple:
|
||||||
"""Execute DELETE FILE command as per TS 102 222 Section 6.4"""
|
"""Execute DELETE FILE command as per TS 102 222 Section 6.4"""
|
||||||
return self.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
|
return self._tp.send_apdu_checksw(self.cla_byte + 'e4000002' + fid)
|
||||||
|
|
||||||
def terminate_df(self, fid: Hexstr) -> ResTuple:
|
def terminate_df(self, fid: Hexstr) -> ResTuple:
|
||||||
"""Execute TERMINATE DF command as per TS 102 222 Section 6.7"""
|
"""Execute TERMINATE DF command as per TS 102 222 Section 6.7"""
|
||||||
return self.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
|
return self._tp.send_apdu_checksw(self.cla_byte + 'e6000002' + fid)
|
||||||
|
|
||||||
def terminate_ef(self, fid: Hexstr) -> ResTuple:
|
def terminate_ef(self, fid: Hexstr) -> ResTuple:
|
||||||
"""Execute TERMINATE EF command as per TS 102 222 Section 6.8"""
|
"""Execute TERMINATE EF command as per TS 102 222 Section 6.8"""
|
||||||
return self.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
|
return self._tp.send_apdu_checksw(self.cla_byte + 'e8000002' + fid)
|
||||||
|
|
||||||
def terminate_card_usage(self) -> ResTuple:
|
def terminate_card_usage(self) -> ResTuple:
|
||||||
"""Execute TERMINATE CARD USAGE command as per TS 102 222 Section 6.9"""
|
"""Execute TERMINATE CARD USAGE command as per TS 102 222 Section 6.9"""
|
||||||
return self.send_apdu_checksw(self.cla_byte + 'fe000000')
|
return self._tp.send_apdu_checksw(self.cla_byte + 'fe000000')
|
||||||
|
|
||||||
def manage_channel(self, mode: str = 'open', lchan_nr: int =0) -> ResTuple:
|
def manage_channel(self, mode: str = 'open', lchan_nr: int =0) -> ResTuple:
|
||||||
"""Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17.
|
"""Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17.
|
||||||
@@ -652,8 +574,8 @@ class SimCardCommands:
|
|||||||
p1 = 0x80
|
p1 = 0x80
|
||||||
else:
|
else:
|
||||||
p1 = 0x00
|
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)
|
return self._tp.send_apdu_checksw(pdu)
|
||||||
|
|
||||||
def reset_card(self) -> Hexstr:
|
def reset_card(self) -> Hexstr:
|
||||||
"""Physically reset the card"""
|
"""Physically reset the card"""
|
||||||
@@ -663,8 +585,8 @@ class SimCardCommands:
|
|||||||
if sw_match(sw, '63cx'):
|
if sw_match(sw, '63cx'):
|
||||||
raise RuntimeError('Failed to %s chv_no 0x%02X with code 0x%s, %i tries left.' %
|
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])))
|
(op_name, chv_no, b2h(pin_code).upper(), int(sw[3])))
|
||||||
if sw != '9000':
|
elif (sw != '9000'):
|
||||||
raise SwMatchError(sw, '9000', self._tp.sw_interpreter)
|
raise SwMatchError(sw, '9000')
|
||||||
|
|
||||||
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
|
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
|
||||||
"""Verify a given CHV (Card Holder Verification == PIN)
|
"""Verify a given CHV (Card Holder Verification == PIN)
|
||||||
@@ -674,7 +596,8 @@ class SimCardCommands:
|
|||||||
code : chv code as hex string
|
code : chv code as hex string
|
||||||
"""
|
"""
|
||||||
fc = rpad(b2h(code), 16)
|
fc = rpad(b2h(code), 16)
|
||||||
data, sw = self.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
|
data, sw = self._tp.send_apdu(
|
||||||
|
self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
|
||||||
self._chv_process_sw('verify', chv_no, code, sw)
|
self._chv_process_sw('verify', chv_no, code, sw)
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|
||||||
@@ -687,7 +610,8 @@ class SimCardCommands:
|
|||||||
pin_code : new chv code as hex string
|
pin_code : new chv code as hex string
|
||||||
"""
|
"""
|
||||||
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
|
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
|
||||||
data, sw = self.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
|
data, sw = self._tp.send_apdu(
|
||||||
|
self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
|
||||||
self._chv_process_sw('unblock', chv_no, pin_code, sw)
|
self._chv_process_sw('unblock', chv_no, pin_code, sw)
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|
||||||
@@ -700,7 +624,8 @@ class SimCardCommands:
|
|||||||
new_pin_code : new chv code as hex string
|
new_pin_code : new chv code as hex string
|
||||||
"""
|
"""
|
||||||
fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16)
|
fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16)
|
||||||
data, sw = self.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
|
data, sw = self._tp.send_apdu(
|
||||||
|
self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
|
||||||
self._chv_process_sw('change', chv_no, pin_code, sw)
|
self._chv_process_sw('change', chv_no, pin_code, sw)
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|
||||||
@@ -713,7 +638,8 @@ class SimCardCommands:
|
|||||||
new_pin_code : new chv code as hex string
|
new_pin_code : new chv code as hex string
|
||||||
"""
|
"""
|
||||||
fc = rpad(b2h(pin_code), 16)
|
fc = rpad(b2h(pin_code), 16)
|
||||||
data, sw = self.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
|
data, sw = self._tp.send_apdu(
|
||||||
|
self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
|
||||||
self._chv_process_sw('disable', chv_no, pin_code, sw)
|
self._chv_process_sw('disable', chv_no, pin_code, sw)
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|
||||||
@@ -725,7 +651,8 @@ class SimCardCommands:
|
|||||||
pin_code : chv code as hex string
|
pin_code : chv code as hex string
|
||||||
"""
|
"""
|
||||||
fc = rpad(b2h(pin_code), 16)
|
fc = rpad(b2h(pin_code), 16)
|
||||||
data, sw = self.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
|
data, sw = self._tp.send_apdu(
|
||||||
|
self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
|
||||||
self._chv_process_sw('enable', chv_no, pin_code, sw)
|
self._chv_process_sw('enable', chv_no, pin_code, sw)
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|
||||||
@@ -735,7 +662,7 @@ class SimCardCommands:
|
|||||||
Args:
|
Args:
|
||||||
payload : payload as hex string
|
payload : payload as hex string
|
||||||
"""
|
"""
|
||||||
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload) + "00", apply_lchan = False)
|
return self._tp.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
|
||||||
|
|
||||||
def terminal_profile(self, payload: Hexstr) -> ResTuple:
|
def terminal_profile(self, payload: Hexstr) -> ResTuple:
|
||||||
"""Send TERMINAL PROFILE to card
|
"""Send TERMINAL PROFILE to card
|
||||||
@@ -744,7 +671,7 @@ class SimCardCommands:
|
|||||||
payload : payload as hex string
|
payload : payload as hex string
|
||||||
"""
|
"""
|
||||||
data_length = len(payload) // 2
|
data_length = len(payload) // 2
|
||||||
data, sw = self.send_apdu_checksw(('80100000%02x' % data_length) + payload, apply_lchan = False)
|
data, sw = self._tp.send_apdu(('80100000%02x' % data_length) + payload)
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|
||||||
# ETSI TS 102 221 11.1.22
|
# ETSI TS 102 221 11.1.22
|
||||||
@@ -758,31 +685,34 @@ class SimCardCommands:
|
|||||||
def encode_duration(secs: int) -> Hexstr:
|
def encode_duration(secs: int) -> Hexstr:
|
||||||
if secs >= 10*24*60*60:
|
if secs >= 10*24*60*60:
|
||||||
return '04%02x' % (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))
|
return '03%02x' % (secs // (24*60*60))
|
||||||
if secs >= 60*60:
|
elif secs >= 60*60:
|
||||||
return '02%02x' % (secs // (60*60))
|
return '02%02x' % (secs // (60*60))
|
||||||
if secs >= 60:
|
elif secs >= 60:
|
||||||
return '01%02x' % (secs // 60)
|
return '01%02x' % (secs // 60)
|
||||||
return '00%02x' % secs
|
else:
|
||||||
|
return '00%02x' % secs
|
||||||
|
|
||||||
def decode_duration(enc: Hexstr) -> int:
|
def decode_duration(enc: Hexstr) -> int:
|
||||||
time_unit = enc[:2]
|
time_unit = enc[:2]
|
||||||
length = h2i(enc[2:4])[0]
|
length = h2i(enc[2:4])[0]
|
||||||
if time_unit == '04':
|
if time_unit == '04':
|
||||||
return length * 10*24*60*60
|
return length * 10*24*60*60
|
||||||
if time_unit == '03':
|
elif time_unit == '03':
|
||||||
return length * 24*60*60
|
return length * 24*60*60
|
||||||
if time_unit == '02':
|
elif time_unit == '02':
|
||||||
return length * 60*60
|
return length * 60*60
|
||||||
if time_unit == '01':
|
elif time_unit == '01':
|
||||||
return length * 60
|
return length * 60
|
||||||
if time_unit == '00':
|
elif time_unit == '00':
|
||||||
return length
|
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)
|
min_dur_enc = encode_duration(min_len_secs)
|
||||||
max_dur_enc = encode_duration(max_len_secs)
|
max_dur_enc = encode_duration(max_len_secs)
|
||||||
data, sw = self.send_apdu_checksw('8076000004' + min_dur_enc + max_dur_enc, apply_lchan = False)
|
data, sw = self._tp.send_apdu_checksw(
|
||||||
|
'8076000004' + min_dur_enc + max_dur_enc)
|
||||||
negotiated_duration_secs = decode_duration(data[:4])
|
negotiated_duration_secs = decode_duration(data[:4])
|
||||||
resume_token = data[4:]
|
resume_token = data[4:]
|
||||||
return (negotiated_duration_secs, resume_token, sw)
|
return (negotiated_duration_secs, resume_token, sw)
|
||||||
@@ -792,15 +722,14 @@ class SimCardCommands:
|
|||||||
"""Send SUSPEND UICC (resume) to the card."""
|
"""Send SUSPEND UICC (resume) to the card."""
|
||||||
if len(h2b(token)) != 8:
|
if len(h2b(token)) != 8:
|
||||||
raise ValueError("Token must be 8 bytes long")
|
raise ValueError("Token must be 8 bytes long")
|
||||||
data, sw = self.send_apdu_checksw('8076010008' + token, apply_lchan = False)
|
data, sw = self._tp.send_apdu_checksw('8076010008' + token)
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|
||||||
# GPC_SPE_034 11.3
|
|
||||||
def get_data(self, tag: int, cla: int = 0x00):
|
def get_data(self, tag: int, cla: int = 0x00):
|
||||||
data, sw = self.send_apdu_checksw('%02xca%04x00' % (cla, tag))
|
data, sw = self._tp.send_apdu('%02xca%04x00' % (cla, tag))
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|
||||||
# TS 31.102 Section 7.5.2
|
# TS 31.102 Section 7.5.2
|
||||||
def get_identity(self, context: int) -> Tuple[Hexstr, SwHexstr]:
|
def get_identity(self, context: int) -> Tuple[Hexstr, SwHexstr]:
|
||||||
data, sw = self.send_apdu_checksw('807800%02x00' % (context))
|
data, sw = self._tp.send_apdu_checksw('807800%02x00' % (context))
|
||||||
return (data, sw)
|
return (data, sw)
|
||||||
|
|||||||
552
pySim/construct.py
Normal file
552
pySim/construct.py
Normal 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)
|
||||||
@@ -1,55 +1,10 @@
|
|||||||
import sys
|
import sys
|
||||||
from typing import Optional, Tuple
|
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
|
|
||||||
class PMO:
|
import asn1tools
|
||||||
"""Convenience conversion class for ProfileManagementOperation as used in ES9+ notifications."""
|
|
||||||
pmo4operation = {
|
|
||||||
'install': 0x80,
|
|
||||||
'enable': 0x40,
|
|
||||||
'disable': 0x20,
|
|
||||||
'delete': 0x10,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, op: str):
|
def compile_asn1_subdir(subdir_name:str):
|
||||||
if not op in self.pmo4operation:
|
|
||||||
raise ValueError('Unknown operation "%s"' % op)
|
|
||||||
self.op = op
|
|
||||||
|
|
||||||
def to_int(self):
|
|
||||||
return self.pmo4operation[self.op]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _num_bits(data: int)-> int:
|
|
||||||
for i in range(0, 8):
|
|
||||||
if data & (1 << i):
|
|
||||||
return 8-i
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def to_bitstring(self) -> Tuple[bytes, int]:
|
|
||||||
"""return value in a format as used by asn1tools for BITSTRING."""
|
|
||||||
val = self.to_int()
|
|
||||||
return (bytes([val]), self._num_bits(val))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_int(cls, i: int) -> 'PMO':
|
|
||||||
"""Parse an integer representation."""
|
|
||||||
for k, v in cls.pmo4operation.items():
|
|
||||||
if v == i:
|
|
||||||
return cls(k)
|
|
||||||
raise ValueError('Unknown PMO 0x%02x' % i)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_bitstring(cls, bstr: Tuple[bytes, int]) -> 'PMO':
|
|
||||||
"""Parse a asn1tools BITSTRING representation."""
|
|
||||||
return cls.from_int(bstr[0][0])
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.op
|
|
||||||
|
|
||||||
def compile_asn1_subdir(subdir_name:str, codec='der'):
|
|
||||||
"""Helper function that compiles ASN.1 syntax from all files within given subdir"""
|
"""Helper function that compiles ASN.1 syntax from all files within given subdir"""
|
||||||
import asn1tools
|
|
||||||
asn_txt = ''
|
asn_txt = ''
|
||||||
__ver = sys.version_info
|
__ver = sys.version_info
|
||||||
if (__ver.major, __ver.minor) >= (3, 9):
|
if (__ver.major, __ver.minor) >= (3, 9):
|
||||||
@@ -58,81 +13,4 @@ def compile_asn1_subdir(subdir_name:str, codec='der'):
|
|||||||
asn_txt += "\n"
|
asn_txt += "\n"
|
||||||
#else:
|
#else:
|
||||||
#print(resources.read_text(__name__, 'asn1/rsp.asn'))
|
#print(resources.read_text(__name__, 'asn1/rsp.asn'))
|
||||||
return asn1tools.compile_string(asn_txt, codec=codec)
|
return asn1tools.compile_string(asn_txt, codec='der')
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
self.hostname = hostname
|
|
||||||
if '$' in token:
|
|
||||||
raise ValueError('$ sign not permitted in token')
|
|
||||||
self.token = token
|
|
||||||
# TODO: validate OID
|
|
||||||
self.oid = oid
|
|
||||||
self.cc_required = cc_required
|
|
||||||
# only format 1 is specified and supported here
|
|
||||||
self.format = 1
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def decode_str(ac: str) -> dict:
|
|
||||||
"""decode an activation code from its string representation."""
|
|
||||||
if ac[0] != '1':
|
|
||||||
raise ValueError("Unsupported AC_Format '%s'!" % ac[0])
|
|
||||||
ac_elements = ac.split('$')
|
|
||||||
d = {
|
|
||||||
'oid': None,
|
|
||||||
'cc_required': False,
|
|
||||||
}
|
|
||||||
d['format'] = ac_elements.pop(0)
|
|
||||||
d['hostname'] = ac_elements.pop(0)
|
|
||||||
d['token'] = ac_elements.pop(0)
|
|
||||||
if len(ac_elements):
|
|
||||||
oid = ac_elements.pop(0)
|
|
||||||
if oid != '':
|
|
||||||
d['oid'] = oid
|
|
||||||
if len(ac_elements):
|
|
||||||
ccr = ac_elements.pop(0)
|
|
||||||
if ccr == '1':
|
|
||||||
d['cc_required'] = True
|
|
||||||
return d
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_string(cls, ac: str) -> 'ActivationCode':
|
|
||||||
"""Create new instance from SGP.22 section 4.1 string representation."""
|
|
||||||
d = cls.decode_str(ac)
|
|
||||||
return cls(d['hostname'], d['token'], d['oid'], d['cc_required'])
|
|
||||||
|
|
||||||
def to_string(self, for_qrcode:bool = False) -> str:
|
|
||||||
"""Convert from internal representation to SGP.22 section 4.1 string representation."""
|
|
||||||
if for_qrcode:
|
|
||||||
ret = 'LPA:'
|
|
||||||
else:
|
|
||||||
ret = ''
|
|
||||||
ret += '%d$%s$%s' % (self.format, self.hostname, self.token)
|
|
||||||
if self.oid:
|
|
||||||
ret += '$%s' % (self.oid)
|
|
||||||
elif self.cc_required:
|
|
||||||
ret += '$'
|
|
||||||
if self.cc_required:
|
|
||||||
ret += '$1'
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.to_string()
|
|
||||||
|
|
||||||
def to_qrcode(self):
|
|
||||||
"""Encode internal representation to QR code."""
|
|
||||||
import qrcode
|
|
||||||
qr = qrcode.QRCode()
|
|
||||||
qr.add_data(self.to_string(for_qrcode=True))
|
|
||||||
return qr.make_image()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "ActivationCode(format=%u, hostname='%s', token='%s', oid=%s, cc_required=%s)" % (self.format,
|
|
||||||
self.hostname,
|
|
||||||
self.token,
|
|
||||||
self.oid,
|
|
||||||
self.cc_required)
|
|
||||||
|
|||||||
@@ -983,7 +983,7 @@ keyAccess [22] OCTET STRING (SIZE (1)) DEFAULT '00'H,
|
|||||||
keyIdentifier [2] OCTET STRING (SIZE (1)),
|
keyIdentifier [2] OCTET STRING (SIZE (1)),
|
||||||
keyVersionNumber [3] OCTET STRING (SIZE (1)),
|
keyVersionNumber [3] OCTET STRING (SIZE (1)),
|
||||||
keyCounterValue [5] OCTET STRING OPTIONAL,
|
keyCounterValue [5] OCTET STRING OPTIONAL,
|
||||||
keyComponents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
|
keyCompontents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
|
||||||
keyType [0] OCTET STRING,
|
keyType [0] OCTET STRING,
|
||||||
keyData [6] OCTET STRING,
|
keyData [6] OCTET STRING,
|
||||||
macLength[7] UInt8 DEFAULT 8
|
macLength[7] UInt8 DEFAULT 8
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
|
# Early proof-of-concept implementation of
|
||||||
where BPP is the Bound Profile Package. So the full expansion is the
|
# GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
|
||||||
"GSMA eSIM Remote SIM Provisioning Bound Profile Packate 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."""
|
#
|
||||||
|
# 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>
|
# (C) 2023 by Harald Welte <laforge@osmocom.org>
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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:
|
# 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).
|
# 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
|
import abc
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -33,17 +36,13 @@ from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
|
|||||||
from Cryptodome.Cipher import AES
|
from Cryptodome.Cipher import AES
|
||||||
from Cryptodome.Hash import CMAC
|
from Cryptodome.Hash import CMAC
|
||||||
|
|
||||||
from osmocom.utils import b2h
|
from pySim.utils import bertlv_encode_len, bertlv_parse_one, b2h
|
||||||
from osmocom.tlv import bertlv_encode_len, bertlv_parse_one
|
|
||||||
|
|
||||||
# don't log by default
|
# don't log by default
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.addHandler(logging.NullHandler())
|
logger.addHandler(logging.NullHandler())
|
||||||
|
|
||||||
MAX_SEGMENT_SIZE = 1020
|
|
||||||
|
|
||||||
class BspAlgo(abc.ABC):
|
class BspAlgo(abc.ABC):
|
||||||
"""Base class representing a cryptographic algorithm within the BSP (BPP Security Protocol)."""
|
|
||||||
blocksize: int
|
blocksize: int
|
||||||
|
|
||||||
def _get_padding(self, in_len: int, multiple: int, padding: int = 0) -> bytes:
|
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:
|
if in_len % multiple == 0:
|
||||||
return b''
|
return b''
|
||||||
pad_cnt = multiple - (in_len % multiple)
|
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:
|
def _pad_to_multiple(self, indat: bytes, multiple: int, padding: int = 0) -> bytes:
|
||||||
"""Pad the input data to multiples of 'multiple'."""
|
"""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):
|
def __str__(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
class BspAlgoCrypt(BspAlgo, abc.ABC):
|
class BspAlgoCrypt(BspAlgo, abc.ABC):
|
||||||
"""Base class representing an encryption/decryption algorithm within the BSP (BPP Security Protocol)."""
|
|
||||||
|
|
||||||
def __init__(self, s_enc: bytes):
|
def __init__(self, s_enc: bytes):
|
||||||
self.s_enc = s_enc
|
self.s_enc = s_enc
|
||||||
@@ -73,7 +71,7 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
|
|||||||
block_nr = self.block_nr
|
block_nr = self.block_nr
|
||||||
ciphertext = self._encrypt(padded_data)
|
ciphertext = self._encrypt(padded_data)
|
||||||
logger.debug("encrypt(block_nr=%u, s_enc=%s, plaintext=%s, padded=%s) -> %s",
|
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
|
return ciphertext
|
||||||
|
|
||||||
def decrypt(self, data:bytes) -> bytes:
|
def decrypt(self, data:bytes) -> bytes:
|
||||||
@@ -83,17 +81,19 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _unpad(self, padded: bytes) -> bytes:
|
def _unpad(self, padded: bytes) -> bytes:
|
||||||
"""Remove the padding from padded data."""
|
"""Remove the padding from padded data."""
|
||||||
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _encrypt(self, data:bytes) -> bytes:
|
def _encrypt(self, data:bytes) -> bytes:
|
||||||
"""Actual implementation, to be implemented by derived class."""
|
"""Actual implementation, to be implemented by derived class."""
|
||||||
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _decrypt(self, data:bytes) -> bytes:
|
def _decrypt(self, data:bytes) -> bytes:
|
||||||
"""Actual implementation, to be implemented by derived class."""
|
"""Actual implementation, to be implemented by derived class."""
|
||||||
|
pass
|
||||||
|
|
||||||
class BspAlgoCryptAES128(BspAlgoCrypt):
|
class BspAlgoCryptAES128(BspAlgoCrypt):
|
||||||
"""AES-CBC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
|
|
||||||
name = 'AES-CBC-128'
|
name = 'AES-CBC-128'
|
||||||
blocksize = 16
|
blocksize = 16
|
||||||
|
|
||||||
@@ -134,7 +134,6 @@ class BspAlgoCryptAES128(BspAlgoCrypt):
|
|||||||
|
|
||||||
|
|
||||||
class BspAlgoMac(BspAlgo, abc.ABC):
|
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
|
l_mac = 0 # must be overridden by derived class
|
||||||
|
|
||||||
def __init__(self, s_mac: bytes, initial_mac_chaining_value: bytes):
|
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
|
temp_data = self.mac_chain + tag_and_length + data
|
||||||
old_mcv = self.mac_chain
|
old_mcv = self.mac_chain
|
||||||
c_mac = self._auth(temp_data)
|
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.
|
# 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
|
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",
|
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
|
return ret
|
||||||
|
|
||||||
def verify(self, ciphertext: bytes) -> bool:
|
def verify(self, ciphertext: bytes) -> bool:
|
||||||
@@ -177,9 +166,9 @@ class BspAlgoMac(BspAlgo, abc.ABC):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _auth(self, temp_data: bytes) -> bytes:
|
def _auth(self, temp_data: bytes) -> bytes:
|
||||||
"""To be implemented by algorithm specific derived class."""
|
"""To be implemented by algorithm specific derived class."""
|
||||||
|
pass
|
||||||
|
|
||||||
class BspAlgoMacAES128(BspAlgoMac):
|
class BspAlgoMacAES128(BspAlgoMac):
|
||||||
"""AES-CMAC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
|
|
||||||
name = 'AES-CMAC-128'
|
name = 'AES-CMAC-128'
|
||||||
l_mac = 8
|
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_enc = out[l:2*l]
|
||||||
s_mac = out[l*2:3*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
|
return s_enc, s_mac, initial_mac_chaining_value
|
||||||
|
|
||||||
|
|
||||||
@@ -243,24 +227,12 @@ class BspInstance:
|
|||||||
return cls(s_enc, s_mac, initial_mcv)
|
return cls(s_enc, s_mac, initial_mcv)
|
||||||
|
|
||||||
def encrypt_and_mac_one(self, tag: int, plaintext:bytes) -> bytes:
|
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 tag <= 255
|
||||||
assert len(plaintext) <= self.max_payload_size
|
assert len(plaintext) <= self.max_payload_size
|
||||||
|
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext))
|
||||||
# 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])
|
|
||||||
ciphered = self.c_algo.encrypt(plaintext)
|
ciphered = self.c_algo.encrypt(plaintext)
|
||||||
logger.debug(f"BSP_DEBUG: ciphered[:20]: {ciphered[:20].hex()}")
|
|
||||||
|
|
||||||
maced = self.m_algo.auth(tag, ciphered)
|
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
|
return maced
|
||||||
|
|
||||||
def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
|
def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
|
||||||
@@ -280,11 +252,11 @@ class BspInstance:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def mac_only_one(self, tag: int, plaintext: bytes) -> bytes:
|
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 tag <= 255
|
||||||
assert len(plaintext) <= self.max_payload_size
|
assert len(plaintext) < self.max_payload_size
|
||||||
maced = self.m_algo.auth(tag, plaintext)
|
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
|
self.c_algo.block_nr += 1
|
||||||
return maced
|
return maced
|
||||||
|
|
||||||
@@ -317,9 +289,7 @@ class BspInstance:
|
|||||||
|
|
||||||
def demac_only_one(self, ciphertext: bytes) -> bytes:
|
def demac_only_one(self, ciphertext: bytes) -> bytes:
|
||||||
payload = self.m_algo.verify(ciphertext)
|
payload = self.m_algo.verify(ciphertext)
|
||||||
_tdict, _l, val, _remain = bertlv_parse_one(payload)
|
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
|
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:
|
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
"""GSMA eSIM RSP ES2+ interface according to SGP.22 v2.5"""
|
|
||||||
|
|
||||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
import time
|
|
||||||
|
|
||||||
from pySim.esim.http_json_api import *
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
class param:
|
|
||||||
class Iccid(ApiParamString):
|
|
||||||
"""String representation of 19 or 20 digits, where the 20th digit MAY optionally be the padding
|
|
||||||
character F."""
|
|
||||||
@classmethod
|
|
||||||
def _encode(cls, data):
|
|
||||||
data = str(data)
|
|
||||||
# SGP.22 version prior to 2.2 do not require support for 19-digit ICCIDs, so let's always
|
|
||||||
# encode it with padding F at the end.
|
|
||||||
if len(data) == 19:
|
|
||||||
data += 'F'
|
|
||||||
return data
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def verify_encoded(cls, data):
|
|
||||||
if len(data) not in [19, 20]:
|
|
||||||
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _decode(cls, data):
|
|
||||||
# strip trailing padding (if it's 20 digits)
|
|
||||||
if len(data) == 20 and data[-1] in ['F', 'f']:
|
|
||||||
data = data[:-1]
|
|
||||||
return data
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def verify_decoded(cls, data):
|
|
||||||
data = str(data)
|
|
||||||
if len(data) not in [19, 20]:
|
|
||||||
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
|
|
||||||
if len(data) == 19:
|
|
||||||
decimal_part = data
|
|
||||||
else:
|
|
||||||
decimal_part = data[:-1]
|
|
||||||
final_part = data[-1:]
|
|
||||||
if final_part not in ['F', 'f'] and not final_part.isdecimal():
|
|
||||||
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
|
|
||||||
if not decimal_part.isdecimal():
|
|
||||||
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
|
|
||||||
|
|
||||||
|
|
||||||
class Eid(ApiParamString):
|
|
||||||
"""String of 32 decimal characters"""
|
|
||||||
@classmethod
|
|
||||||
def verify_encoded(cls, data):
|
|
||||||
if len(data) != 32:
|
|
||||||
raise ValueError('EID length invalid: "%s" (%u)' % (data, len(data)))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def verify_decoded(cls, data):
|
|
||||||
if not data.isdecimal():
|
|
||||||
raise ValueError('EID (%s) contains non-decimal characters' % data)
|
|
||||||
|
|
||||||
class ProfileType(ApiParamString):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MatchingId(ApiParamString):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ConfirmationCode(ApiParamString):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SmdsAddress(ApiParamFqdn):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ReleaseFlag(ApiParamBoolean):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class FinalProfileStatusIndicator(ApiParamString):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Timestamp(ApiParamString):
|
|
||||||
"""String format as specified by W3C: YYYY-MM-DDThh:mm:ssTZD"""
|
|
||||||
@classmethod
|
|
||||||
def _decode(cls, data):
|
|
||||||
return datetime.fromisoformat(data)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _encode(cls, data):
|
|
||||||
return datetime.isoformat(data)
|
|
||||||
|
|
||||||
class NotificationPointId(ApiParamInteger):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NotificationPointStatus(ApiParam):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ResultData(ApiParamBase64):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Es2PlusApiFunction(JsonHttpApiFunction):
|
|
||||||
"""Base 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())
|
|
||||||
@@ -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>
|
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
from pySim.utils import b2h, h2b, bertlv_encode_tag, bertlv_encode_len
|
||||||
from osmocom.utils import b2h, h2b
|
|
||||||
from osmocom.tlv import bertlv_encode_tag, bertlv_encode_len, bertlv_parse_one_rawtag
|
|
||||||
from osmocom.tlv import bertlv_return_one_rawtlv
|
|
||||||
|
|
||||||
import pySim.esim.rsp as rsp
|
import pySim.esim.rsp as rsp
|
||||||
from pySim.esim.bsp import BspInstance
|
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
|
# 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
|
# 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:
|
class ProfileMetadata:
|
||||||
"""Representation of Profile metadata. Right now only the mandatory bits are
|
"""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"""
|
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.iccid_bin = iccid_bin
|
||||||
self.spn = spn
|
self.spn = spn
|
||||||
self.profile_name = profile_name
|
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:
|
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 = {
|
smr = {
|
||||||
'iccid': self.iccid_bin,
|
'iccid': self.iccid_bin,
|
||||||
'serviceProviderName': self.spn,
|
'serviceProviderName': self.spn,
|
||||||
'profileName': self.profile_name,
|
'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)
|
return rsp.asn1.encode('StoreMetadataRequest', smr)
|
||||||
|
|
||||||
|
|
||||||
@@ -208,12 +167,8 @@ class BoundProfilePackage(ProfilePackage):
|
|||||||
# 'initialiseSecureChannelRequest'
|
# 'initialiseSecureChannelRequest'
|
||||||
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
|
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
|
||||||
# firstSequenceOf87
|
# 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))
|
bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
|
||||||
# sequenceOF88
|
# sequenceOF88
|
||||||
logger.debug("BPP_ENCODE_DEBUG: MAC-only StoreMetadata with BSP keys")
|
|
||||||
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
|
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
|
||||||
|
|
||||||
if self.ppp: # we have to use session keys
|
if self.ppp: # we have to use session keys
|
||||||
@@ -228,79 +183,3 @@ class BoundProfilePackage(ProfilePackage):
|
|||||||
|
|
||||||
# manual DER encode: wrap in outer SEQUENCE
|
# manual DER encode: wrap in outer SEQUENCE
|
||||||
return bertlv_encode_tag(0xbf36) + bertlv_encode_len(len(bpp_seq)) + bpp_seq
|
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
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
"""GSMA eSIM RSP HTTP/REST/JSON interface according to SGP.22 v2.5"""
|
|
||||||
|
|
||||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import requests
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from typing import Optional
|
|
||||||
import base64
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
class ApiParam(abc.ABC):
|
|
||||||
"""A class 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)
|
|
||||||
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:
|
|
||||||
return self.decode(response.json())
|
|
||||||
return None
|
|
||||||
@@ -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>
|
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
@@ -18,12 +19,12 @@
|
|||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import shelve
|
import shelve
|
||||||
|
import copyreg
|
||||||
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding
|
from cryptography.hazmat.primitives.serialization import Encoding
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from osmocom.utils import b2h
|
from collections.abc import MutableMapping
|
||||||
from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv
|
|
||||||
|
|
||||||
from pySim.esim import compile_asn1_subdir
|
from pySim.esim import compile_asn1_subdir
|
||||||
|
|
||||||
@@ -34,11 +35,10 @@ class RspSessionState:
|
|||||||
and subsequently used by further API calls using the same transactionId. The session state
|
and subsequently used by further API calls using the same transactionId. The session state
|
||||||
is removed either after cancelSession or after notification.
|
is removed either after cancelSession or after notification.
|
||||||
TODO: add some kind of time based expiration / garbage collection."""
|
TODO: add some kind of time based expiration / garbage collection."""
|
||||||
def __init__(self, transactionId: str, serverChallenge: bytes, ci_cert_id: bytes):
|
def __init__(self, transactionId: str, serverChallenge: bytes):
|
||||||
self.transactionId = transactionId
|
self.transactionId = transactionId
|
||||||
self.serverChallenge = serverChallenge
|
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.euicc_cert: Optional[x509.Certificate] = None
|
||||||
self.eum_cert: Optional[x509.Certificate] = None
|
self.eum_cert: Optional[x509.Certificate] = None
|
||||||
self.eid: Optional[bytes] = None
|
self.eid: Optional[bytes] = None
|
||||||
@@ -90,90 +90,11 @@ class RspSessionState:
|
|||||||
# FIXME: how to add the public key from smdp_otpk to an instance of EllipticCurvePrivateKey?
|
# FIXME: how to add the public key from smdp_otpk to an instance of EllipticCurvePrivateKey?
|
||||||
del state['_smdp_otsk']
|
del state['_smdp_otsk']
|
||||||
del state['_smdp_ot_curve']
|
del state['_smdp_ot_curve']
|
||||||
# automatically recover all the remaining state
|
# automatically recover all the remainig state
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
|
|
||||||
|
|
||||||
class RspSessionStore:
|
class RspSessionStore(shelve.DbfilenameShelf):
|
||||||
"""A wrapper around the database-backed storage 'shelve' for storing RspSessionState objects.
|
"""A derived class as wrapper around the database-backed non-volatile storage 'shelve', in case we might
|
||||||
Can be configured to use either file-based storage or in-memory storage.
|
need to extend it in the future. We use it to store RspSessionState objects indexed by transactionId."""
|
||||||
We use it to store RspSessionState objects indexed by transactionId."""
|
pass
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,120 +0,0 @@
|
|||||||
"""Implementation of SimAlliance/TCA Interoperable Profile OIDs"""
|
|
||||||
|
|
||||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from functools import total_ordering
|
|
||||||
from typing import List, Union
|
|
||||||
|
|
||||||
@total_ordering
|
|
||||||
class OID:
|
|
||||||
@staticmethod
|
|
||||||
def intlist_from_str(instr: str) -> List[int]:
|
|
||||||
return [int(x) for x in instr.split('.')]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def str_from_intlist(intlist: List[int]) -> str:
|
|
||||||
return '.'.join([str(x) for x in intlist])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def highest_oid(oids: List['OID']) -> 'OID':
|
|
||||||
return sorted(oids)[-1]
|
|
||||||
|
|
||||||
def __init__(self, initializer: Union[List[int], str]):
|
|
||||||
if isinstance(initializer, str):
|
|
||||||
self.intlist = self.intlist_from_str(initializer)
|
|
||||||
else:
|
|
||||||
self.intlist = initializer
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.str_from_intlist(self.intlist)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return 'OID(%s)' % (str(self))
|
|
||||||
|
|
||||||
def __eq__(self, other: 'OID'):
|
|
||||||
return (self.intlist == other.intlist)
|
|
||||||
|
|
||||||
def __ne__(self, other: 'OID'):
|
|
||||||
# implement based on __eq__
|
|
||||||
return not (self == other)
|
|
||||||
|
|
||||||
def cmp(self, other: 'OID'):
|
|
||||||
self_len = len(self.intlist)
|
|
||||||
other_len = len(other.intlist)
|
|
||||||
common_len = min(self_len, other_len)
|
|
||||||
max_len = max(self_len, other_len)
|
|
||||||
|
|
||||||
for i in range(0, max_len+1):
|
|
||||||
if i >= self_len:
|
|
||||||
# other list is longer
|
|
||||||
return -1
|
|
||||||
if i >= other_len:
|
|
||||||
# our list is longer
|
|
||||||
return 1
|
|
||||||
if self.intlist[i] > other.intlist[i]:
|
|
||||||
# our version is higher
|
|
||||||
return 1
|
|
||||||
if self.intlist[i] < other.intlist[i]:
|
|
||||||
# other version is higher
|
|
||||||
return -1
|
|
||||||
# continue to next digit
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def __gt__(self, other: 'OID'):
|
|
||||||
if self.cmp(other) > 0:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def prefix_match(self, oid_str: Union[str, 'OID']):
|
|
||||||
"""determine if oid_str is equal or below our OID."""
|
|
||||||
return str(oid_str).startswith(str(self))
|
|
||||||
|
|
||||||
|
|
||||||
class eOID(OID):
|
|
||||||
"""OID helper for TCA eUICC prefix"""
|
|
||||||
__prefix = [2,23,143,1]
|
|
||||||
def __init__(self, initializer):
|
|
||||||
if isinstance(initializer, str):
|
|
||||||
initializer = self.intlist_from_str(initializer)
|
|
||||||
super().__init__(self.__prefix + initializer)
|
|
||||||
|
|
||||||
MF = eOID("2.1")
|
|
||||||
DF_CD = eOID("2.2")
|
|
||||||
DF_TELECOM = eOID("2.3")
|
|
||||||
DF_TELECOM_v2 = eOID("2.3.2")
|
|
||||||
ADF_USIM_by_default = eOID("2.4")
|
|
||||||
ADF_USIM_by_default_v2 = eOID("2.4.2")
|
|
||||||
ADF_USIMopt_not_by_default = eOID("2.5")
|
|
||||||
ADF_USIMopt_not_by_default_v2 = eOID("2.5.2")
|
|
||||||
ADF_USIMopt_not_by_default_v3 = eOID("2.5.3")
|
|
||||||
DF_PHONEBOOK_ADF_USIM = eOID("2.6")
|
|
||||||
DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
|
|
||||||
ADF_ISIM_by_default = eOID("2.8")
|
|
||||||
ADF_ISIMopt_not_by_default = eOID("2.9")
|
|
||||||
ADF_ISIMopt_not_by_default_v2 = eOID("2.9.2")
|
|
||||||
ADF_CSIM_by_default = eOID("2.10")
|
|
||||||
ADF_CSIM_by_default_v2 = eOID("2.10.2")
|
|
||||||
ADF_CSIMopt_not_by_default = eOID("2.11")
|
|
||||||
ADF_CSIMopt_not_by_default_v2 = eOID("2.11.2")
|
|
||||||
DF_EAP = eOID("2.12")
|
|
||||||
DF_5GS = eOID("2.13")
|
|
||||||
DF_5GS_v2 = eOID("2.13.2")
|
|
||||||
DF_5GS_v3 = eOID("2.13.3")
|
|
||||||
DF_5GS_v4 = eOID("2.13.4")
|
|
||||||
DF_SAIP = eOID("2.14")
|
|
||||||
DF_SNPN = eOID("2.15")
|
|
||||||
DF_5GProSe = eOID("2.16")
|
|
||||||
IoT_by_default = eOID("2.17")
|
|
||||||
IoTopt_not_by_default = eOID("2.18")
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Implementation of Personalization of eSIM profiles in SimAlliance/TCA Interoperable Profile."""
|
# Implementation of SimAlliance/TCA Interoperable Profile handling
|
||||||
|
#
|
||||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
@@ -16,189 +16,57 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import io
|
from typing import List, Tuple, Optional
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
from osmocom.tlv import camel_to_snake
|
|
||||||
from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
|
|
||||||
from pySim.esim.saip import ProfileElement, ProfileElementSequence
|
from pySim.esim.saip import ProfileElement, ProfileElementSequence
|
||||||
|
|
||||||
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) -> List[Tuple]:
|
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_key:str) -> List[Tuple]:
|
||||||
"""In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
|
"""In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
|
||||||
return list(filter(lambda x: x[0] not in unwanted_keys, l))
|
return list(filter(lambda x: x[0] != unwanted_key, l))
|
||||||
|
|
||||||
def file_replace_content(file: List[Tuple], new_content: bytes):
|
def file_replace_content(file: List[Tuple], new_content: bytes):
|
||||||
"""Completely replace all fillFileContent of a decoded 'File' with the new_content."""
|
"""Completely replace all fillFileContent of a decoded 'File' with the new_content."""
|
||||||
# use [:] to avoid making a copy, as we're doing in-place modification of the list here
|
file = remove_unwanted_tuples_from_list(file, 'fillFileContent')
|
||||||
file[:] = remove_unwanted_tuples_from_list(file, ['fillFileContent', 'fillFileOffset'])
|
|
||||||
file.append(('fillFileContent', new_content))
|
file.append(('fillFileContent', new_content))
|
||||||
return file
|
return file
|
||||||
|
|
||||||
class ClassVarMeta(abc.ABCMeta):
|
class ClassVarMeta(abc.ABCMeta):
|
||||||
"""Metaclass that puts all additional keyword-args into the class. We use this to have one
|
"""Metaclass that puts all additional keyword-args into the class."""
|
||||||
class definition for something like a PIN, and then have derived classes for PIN1, PIN2, ..."""
|
|
||||||
def __new__(metacls, name, bases, namespace, **kwargs):
|
def __new__(metacls, name, bases, namespace, **kwargs):
|
||||||
#print("Meta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
|
#print("Meta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
|
||||||
x = super().__new__(metacls, name, bases, namespace)
|
x = super().__new__(metacls, name, bases, namespace)
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
setattr(x, k, v)
|
setattr(x, k, v)
|
||||||
setattr(x, 'name', camel_to_snake(name))
|
|
||||||
return x
|
return x
|
||||||
|
|
||||||
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
||||||
"""Base class representing a part of the eSIM profile that is configurable during the
|
"""Base class representing a part of the eSIM profile that is configurable during the
|
||||||
personalization process (with dynamic data from elsewhere)."""
|
personalization process (with dynamic data from elsewhere)."""
|
||||||
def __init__(self, input_value):
|
def __init__(self, value):
|
||||||
self.input_value = input_value # the raw input value as given by caller
|
self.value = value
|
||||||
self.value = None # the processed input value (e.g. with check digit) as produced by validate()
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
"""Optional validation method. Can be used by derived classes to perform validation
|
|
||||||
of the input value (self.value). Will raise an exception if validation fails."""
|
|
||||||
# default implementation: simply copy input_value over to value
|
|
||||||
self.value = self.input_value
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pe_seq: ProfileElementSequence):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Iccid(ConfigurableParameter):
|
class Iccid(ConfigurableParameter):
|
||||||
"""Configurable ICCID. Expects the value to be a string of decimal digits.
|
"""Configurable ICCID. Expects the value to be in EF.ICCID format."""
|
||||||
If the string of digits is only 18 digits long, a Luhn check digit will be added."""
|
name = 'iccid'
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
# convert to string as it might be an integer
|
|
||||||
iccid_str = str(self.input_value)
|
|
||||||
if len(iccid_str) < 18 or len(iccid_str) > 20:
|
|
||||||
raise ValueError('ICCID must be 18, 19 or 20 digits long')
|
|
||||||
if not iccid_str.isdecimal():
|
|
||||||
raise ValueError('ICCID must only contain decimal digits')
|
|
||||||
self.value = sanitize_iccid(iccid_str)
|
|
||||||
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
# patch the header
|
# patch the header; FIXME: swap nibbles!
|
||||||
pes.get_pe_for_type('header').decoded['iccid'] = h2b(rpad(self.value, 20))
|
pes.get_pe_by_type('header').decoded['iccid'] = self.value
|
||||||
# patch MF/EF.ICCID
|
# patch MF/EF.ICCID
|
||||||
file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(self.value)))
|
file_replace_content(pes.get_pe_by_type('mf').decoded['ef-iccid'], self.value)
|
||||||
|
|
||||||
class Imsi(ConfigurableParameter):
|
class Imsi(ConfigurableParameter):
|
||||||
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
|
"""Configurable IMSI. Expects value to be n EF.IMSI format."""
|
||||||
the last digit of the IMSI."""
|
name = 'imsi'
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
# convert to string as it might be an integer
|
|
||||||
imsi_str = str(self.input_value)
|
|
||||||
if len(imsi_str) < 6 or len(imsi_str) > 15:
|
|
||||||
raise ValueError('IMSI must be 6..15 digits long')
|
|
||||||
if not imsi_str.isdecimal():
|
|
||||||
raise ValueError('IMSI must only contain decimal digits')
|
|
||||||
self.value = imsi_str
|
|
||||||
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
imsi_str = self.value
|
|
||||||
# we always use the least significant byte of the IMSI as ACC
|
|
||||||
acc = (1 << int(imsi_str[-1]))
|
|
||||||
# patch ADF.USIM/EF.IMSI
|
# patch ADF.USIM/EF.IMSI
|
||||||
for pe in pes.get_pes_for_type('usim'):
|
for pe in pes.get_pes_by_type('usim'):
|
||||||
file_replace_content(pe.decoded['ef-imsi'], h2b(enc_imsi(imsi_str)))
|
file_replace_content(pe.decoded['ef-imsi'], self.value)
|
||||||
file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
|
|
||||||
# TODO: DF.GSM_ACCESS if not linked?
|
# TODO: DF.GSM_ACCESS if not linked?
|
||||||
|
|
||||||
|
|
||||||
class SdKey(ConfigurableParameter, metaclass=ClassVarMeta):
|
|
||||||
"""Configurable Security Domain (SD) Key. Value is presented as bytes."""
|
|
||||||
# these will be set by derived classes
|
|
||||||
key_type = None
|
|
||||||
key_id = None
|
|
||||||
kvn = None
|
|
||||||
key_usage_qual = None
|
|
||||||
permitted_len = []
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
|
|
||||||
raise ValueError('Value must be of bytes-like type')
|
|
||||||
if self.permitted_len:
|
|
||||||
if len(self.input_value) not in self.permitted_len:
|
|
||||||
raise ValueError('Value length must be %s' % self.permitted_len)
|
|
||||||
self.value = self.input_value
|
|
||||||
|
|
||||||
def _apply_sd(self, pe: ProfileElement):
|
|
||||||
assert pe.type == 'securityDomain'
|
|
||||||
for key in pe.decoded['keyList']:
|
|
||||||
if key['keyIdentifier'][0] == self.key_id and key['keyVersionNumber'][0] == self.kvn:
|
|
||||||
assert len(key['keyComponents']) == 1
|
|
||||||
key['keyComponents'][0]['keyData'] = self.value
|
|
||||||
return
|
|
||||||
# Could not find matching key to patch, create a new one
|
|
||||||
key = {
|
|
||||||
'keyUsageQualifier': bytes([self.key_usage_qual]),
|
|
||||||
'keyIdentifier': bytes([self.key_id]),
|
|
||||||
'keyVersionNumber': bytes([self.kvn]),
|
|
||||||
'keyComponents': [
|
|
||||||
{ 'keyType': bytes([self.key_type]), 'keyData': self.value },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
pe.decoded['keyList'].append(key)
|
|
||||||
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
|
||||||
for pe in pes.get_pes_for_type('securityDomain'):
|
|
||||||
self._apply_sd(pe)
|
|
||||||
|
|
||||||
class SdKeyScp80_01(SdKey, kvn=0x01, key_type=0x88, permitted_len=[16,24,32]): # AES key type
|
|
||||||
pass
|
|
||||||
class SdKeyScp80_01Kic(SdKeyScp80_01, key_id=0x01, key_usage_qual=0x18): # FIXME: ordering?
|
|
||||||
pass
|
|
||||||
class SdKeyScp80_01Kid(SdKeyScp80_01, key_id=0x02, key_usage_qual=0x14):
|
|
||||||
pass
|
|
||||||
class SdKeyScp80_01Kik(SdKeyScp80_01, key_id=0x03, key_usage_qual=0x48):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SdKeyScp81_01(SdKey, kvn=0x81): # FIXME
|
|
||||||
pass
|
|
||||||
class SdKeyScp81_01Psk(SdKeyScp81_01, key_id=0x01, key_type=0x85, key_usage_qual=0x3C):
|
|
||||||
pass
|
|
||||||
class SdKeyScp81_01Dek(SdKeyScp81_01, key_id=0x02, key_type=0x88, key_usage_qual=0x48):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SdKeyScp02_20(SdKey, kvn=0x20, key_type=0x88, permitted_len=[16,24,32]): # AES key type
|
|
||||||
pass
|
|
||||||
class SdKeyScp02_20Enc(SdKeyScp02_20, key_id=0x01, key_usage_qual=0x18):
|
|
||||||
pass
|
|
||||||
class SdKeyScp02_20Mac(SdKeyScp02_20, key_id=0x02, key_usage_qual=0x14):
|
|
||||||
pass
|
|
||||||
class SdKeyScp02_20Dek(SdKeyScp02_20, key_id=0x03, key_usage_qual=0x48):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SdKeyScp03_30(SdKey, kvn=0x30, key_type=0x88, permitted_len=[16,24,32]): # AES key type
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_30Enc(SdKeyScp03_30, key_id=0x01, key_usage_qual=0x18):
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_30Mac(SdKeyScp03_30, key_id=0x02, key_usage_qual=0x14):
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_30Dek(SdKeyScp03_30, key_id=0x03, key_usage_qual=0x48):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SdKeyScp03_31(SdKey, kvn=0x31, key_type=0x88, permitted_len=[16,24,32]): # AES key type
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_31Enc(SdKeyScp03_31, key_id=0x01, key_usage_qual=0x18):
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_31Mac(SdKeyScp03_31, key_id=0x02, key_usage_qual=0x14):
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_31Dek(SdKeyScp03_31, key_id=0x03, key_usage_qual=0x48):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SdKeyScp03_32(SdKey, kvn=0x32, key_type=0x88, permitted_len=[16,24,32]): # AES key type
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_32Enc(SdKeyScp03_32, key_id=0x01, key_usage_qual=0x18):
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_32Mac(SdKeyScp03_32, key_id=0x02, key_usage_qual=0x14):
|
|
||||||
pass
|
|
||||||
class SdKeyScp03_32Dek(SdKeyScp03_32, key_id=0x03, key_usage_qual=0x48):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def obtain_singleton_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
|
def obtain_singleton_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
|
||||||
filtered = list(filter(lambda x: x.type == wanted_type, l))
|
filtered = list(filter(lambda x: x.type == wanted_type, l))
|
||||||
assert len(filtered) == 1
|
assert len(filtered) == 1
|
||||||
@@ -209,25 +77,13 @@ def obtain_first_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> Pr
|
|||||||
return filtered[0]
|
return filtered[0]
|
||||||
|
|
||||||
class Puk(ConfigurableParameter, metaclass=ClassVarMeta):
|
class Puk(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||||
"""Configurable PUK (Pin Unblock Code). String ASCII-encoded digits."""
|
|
||||||
keyReference = None
|
keyReference = None
|
||||||
def validate(self):
|
|
||||||
if isinstance(self.input_value, int):
|
|
||||||
self.value = '%08d' % self.input_value
|
|
||||||
else:
|
|
||||||
self.value = self.input_value
|
|
||||||
# FIXME: valid length?
|
|
||||||
if not self.value.isdecimal():
|
|
||||||
raise ValueError('PUK must only contain decimal digits')
|
|
||||||
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
puk = ''.join(['%02x' % (ord(x)) for x in self.value])
|
|
||||||
padded_puk = rpad(puk, 16)
|
|
||||||
mf_pes = pes.pes_by_naa['mf'][0]
|
mf_pes = pes.pes_by_naa['mf'][0]
|
||||||
pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
|
pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
|
||||||
for pukCode in pukCodes.decoded['pukCodes']:
|
for pukCode in pukCodes.decoded['pukCodes']:
|
||||||
if pukCode['keyReference'] == self.keyReference:
|
if pukCode['keyReference'] == self.keyReference:
|
||||||
pukCode['pukValue'] = h2b(padded_puk)
|
pukCode['pukValue'] = self.value
|
||||||
return
|
return
|
||||||
raise ValueError('cannot find pukCode')
|
raise ValueError('cannot find pukCode')
|
||||||
class Puk1(Puk, keyReference=0x01):
|
class Puk1(Puk, keyReference=0x01):
|
||||||
@@ -236,52 +92,29 @@ class Puk2(Puk, keyReference=0x81):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
|
class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||||
"""Configurable PIN (Personal Identification Number). String of digits."""
|
|
||||||
keyReference = None
|
keyReference = None
|
||||||
def validate(self):
|
|
||||||
if isinstance(self.input_value, int):
|
|
||||||
self.value = '%04d' % self.input_value
|
|
||||||
else:
|
|
||||||
self.value = self.input_value
|
|
||||||
if len(self.value) < 4 or len(self.value) > 8:
|
|
||||||
raise ValueError('PIN mus be 4..8 digits long')
|
|
||||||
if not self.value.isdecimal():
|
|
||||||
raise ValueError('PIN must only contain decimal digits')
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
pin = ''.join(['%02x' % (ord(x)) for x in self.value])
|
|
||||||
padded_pin = rpad(pin, 16)
|
|
||||||
mf_pes = pes.pes_by_naa['mf'][0]
|
mf_pes = pes.pes_by_naa['mf'][0]
|
||||||
pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
|
pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
|
||||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||||
return
|
return
|
||||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||||
if pinCode['keyReference'] == self.keyReference:
|
if pinCode['keyReference'] == self.keyReference:
|
||||||
pinCode['pinValue'] = h2b(padded_pin)
|
pinCode['pinValue'] = self.value
|
||||||
return
|
return
|
||||||
raise ValueError('cannot find pinCode')
|
raise ValueError('cannot find pinCode')
|
||||||
class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
|
class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||||
"""Configurable PIN (Personal Identification Number). String of digits."""
|
|
||||||
keyReference = None
|
keyReference = None
|
||||||
def validate(self):
|
|
||||||
if isinstance(self.input_value, int):
|
|
||||||
self.value = '%04d' % self.input_value
|
|
||||||
else:
|
|
||||||
self.value = self.input_value
|
|
||||||
if len(self.value) < 4 or len(self.value) > 8:
|
|
||||||
raise ValueError('PIN mus be 4..8 digits long')
|
|
||||||
if not self.value.isdecimal():
|
|
||||||
raise ValueError('PIN must only contain decimal digits')
|
|
||||||
def _apply_one(self, pe: ProfileElement):
|
def _apply_one(self, pe: ProfileElement):
|
||||||
pin = ''.join(['%02x' % (ord(x)) for x in self.value])
|
|
||||||
padded_pin = rpad(pin, 16)
|
|
||||||
pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes')
|
pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes')
|
||||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||||
return
|
return
|
||||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||||
if pinCode['keyReference'] == self.keyReference:
|
if pinCode['keyReference'] == self.keyReference:
|
||||||
pinCode['pinValue'] = h2b(padded_pin)
|
pinCode['pinValue'] = self.value
|
||||||
return
|
return
|
||||||
raise ValueError('cannot find pinCode')
|
raise ValueError('cannot find pinCode')
|
||||||
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
for naa in pes.pes_by_naa:
|
for naa in pes.pes_by_naa:
|
||||||
if naa not in ['usim','isim','csim','telecom']:
|
if naa not in ['usim','isim','csim','telecom']:
|
||||||
@@ -300,12 +133,7 @@ class Adm2(Pin, keyReference=0x0B):
|
|||||||
|
|
||||||
|
|
||||||
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
|
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||||
"""Configurable Algorithm parameter."""
|
|
||||||
key = None
|
key = None
|
||||||
def validate(self):
|
|
||||||
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
|
|
||||||
raise ValueError('Value must be of bytes-like type')
|
|
||||||
self.value = self.input_value
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
def apply(self, pes: ProfileElementSequence):
|
||||||
for pe in pes.get_pes_for_type('akaParameter'):
|
for pe in pes.get_pes_for_type('akaParameter'):
|
||||||
algoConfiguration = pe.decoded['algoConfiguration']
|
algoConfiguration = pe.decoded['algoConfiguration']
|
||||||
@@ -318,7 +146,5 @@ class K(AlgoConfig, key='key'):
|
|||||||
class Opc(AlgoConfig, key='opc'):
|
class Opc(AlgoConfig, key='opc'):
|
||||||
pass
|
pass
|
||||||
class AlgorithmID(AlgoConfig, key='algorithmID'):
|
class AlgorithmID(AlgoConfig, key='algorithmID'):
|
||||||
def validate(self):
|
pass
|
||||||
if self.input_value not in [1, 2, 3]:
|
|
||||||
raise ValueError('Invalid algorithmID %s' % (self.input_value))
|
|
||||||
self.value = self.input_value
|
|
||||||
|
|||||||
@@ -1,975 +0,0 @@
|
|||||||
"""Implementation of SimAlliance/TCA Interoperable Profile Templates."""
|
|
||||||
|
|
||||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from typing import *
|
|
||||||
from copy import deepcopy
|
|
||||||
from pySim.utils import all_subclasses, h2b
|
|
||||||
from pySim.filesystem import Path
|
|
||||||
import pySim.esim.saip.oid as OID
|
|
||||||
|
|
||||||
class FileTemplate:
|
|
||||||
"""Representation of a single file in a SimAlliance/TCA Profile Template. The argument order
|
|
||||||
is done to match that of the tables in Section 9 of the SAIP specification."""
|
|
||||||
def __init__(self, fid:int, name:str, ftype, nb_rec: Optional[int], size:Optional[int], arr:int,
|
|
||||||
sfi:Optional[int] = None, default_val:Optional[str] = None, content_rqd:bool = True,
|
|
||||||
params:Optional[List] = None, ass_serv:Optional[List[int]]=None, high_update:bool = False,
|
|
||||||
pe_name:Optional[str] = None, repeat:bool = False, ppath: List[int] = []):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
fid: The 16bit file-identifier of the file
|
|
||||||
name: The name of the file in human-readable "EF.FOO", "DF.BAR" notation
|
|
||||||
ftype: The type of the file; can be 'MF', 'ADF', 'DF', 'TR', 'LF', 'CY', 'BT'
|
|
||||||
nb_rec: Then number of records (only valid for 'LF' and 'CY')
|
|
||||||
size: The size of the file ('TR', 'BT'); size of each record ('LF, 'CY')
|
|
||||||
arr: The record number of EF.ARR for referenced access rules
|
|
||||||
sfi: The short file identifier, if any
|
|
||||||
default_val: The default value [pattern] of the file
|
|
||||||
content_rqd: Whether an instance of template *must* specify file contents
|
|
||||||
params: A list of parameters that an instance of the template *must* specify
|
|
||||||
ass_serv: The associated service[s] of the service table
|
|
||||||
high_update: Is this file of "high update frequency" type?
|
|
||||||
pe_name: The name of this file in the ASN.1 type of the PE. Auto-generated for most.
|
|
||||||
repeat: Whether the default_val pattern is a repeating pattern.
|
|
||||||
ppath: The intermediate path between the base_df of the ProfileTemplate and this file. If not
|
|
||||||
specified, the file will be created immediately underneath the base_df.
|
|
||||||
"""
|
|
||||||
# initialize from arguments
|
|
||||||
self.fid = fid
|
|
||||||
self.name = name
|
|
||||||
if pe_name:
|
|
||||||
self.pe_name = pe_name
|
|
||||||
else:
|
|
||||||
self.pe_name = self.name.replace('.','-').replace('_','-').lower()
|
|
||||||
self.file_type = ftype
|
|
||||||
if ftype in ['LF', 'CY']:
|
|
||||||
self.nb_rec = nb_rec
|
|
||||||
self.rec_len = size
|
|
||||||
elif ftype in ['TR', 'BT']:
|
|
||||||
self.file_size = size
|
|
||||||
self.arr = arr
|
|
||||||
self.sfi = sfi
|
|
||||||
self.default_val = default_val
|
|
||||||
self.default_val_repeat = repeat
|
|
||||||
self.content_rqd = content_rqd
|
|
||||||
self.params = params
|
|
||||||
self.ass_serv = ass_serv
|
|
||||||
self.high_update = high_update
|
|
||||||
self.ppath = ppath # parent path, if this FileTemplate is not immediately below the base_df
|
|
||||||
# initialize empty
|
|
||||||
self.parent = None
|
|
||||||
self.children = []
|
|
||||||
if self.default_val:
|
|
||||||
length = self._default_value_len() or 100
|
|
||||||
# run the method once to verify the pattern can be processed
|
|
||||||
self.expand_default_value_pattern(length)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "FileTemplate(%s)" % (self.name)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
s_fid = "%04x" % self.fid if self.fid is not None else 'None'
|
|
||||||
s_arr = self.arr if self.arr is not None else 'None'
|
|
||||||
s_sfi = "%02x" % self.sfi if self.sfi is not None else 'None'
|
|
||||||
return "FileTemplate(%s/%s, %s, %s, arr=%s, sfi=%s, ppath=%s)" % (self.name, self.pe_name, s_fid, self.file_type, s_arr, s_sfi, self.ppath)
|
|
||||||
|
|
||||||
def print_tree(self, indent:str = ""):
|
|
||||||
"""recursive printing of FileTemplate tree structure."""
|
|
||||||
print("%s%s (%s)" % (indent, repr(self), self.path))
|
|
||||||
indent += " "
|
|
||||||
for c in self.children:
|
|
||||||
c.print_tree(indent)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self):
|
|
||||||
"""Return the path of the given File within the hierarchy."""
|
|
||||||
if self.parent:
|
|
||||||
return self.parent.path + self.name
|
|
||||||
else:
|
|
||||||
return Path(self.name)
|
|
||||||
|
|
||||||
def get_file_by_path(self, path: List[str]) -> Optional['FileTemplate']:
|
|
||||||
"""Return a FileTemplate matching the given path within this ProfileTemplate."""
|
|
||||||
if path[0].lower() != self.name.lower():
|
|
||||||
return None
|
|
||||||
for c in self.children:
|
|
||||||
if path[1].lower() == c.name.lower():
|
|
||||||
return c.get_file_by_path(path[1:])
|
|
||||||
|
|
||||||
def _default_value_len(self):
|
|
||||||
if self.file_type in ['TR']:
|
|
||||||
return self.file_size
|
|
||||||
elif self.file_type in ['LF', 'CY']:
|
|
||||||
return self.rec_len
|
|
||||||
|
|
||||||
def expand_default_value_pattern(self, length: Optional[int] = None) -> Optional[bytes]:
|
|
||||||
"""Expand the default value pattern to the specified length."""
|
|
||||||
if length is None:
|
|
||||||
length = self._default_value_len()
|
|
||||||
if length is None:
|
|
||||||
raise ValueError("%s does not have a default length" % self)
|
|
||||||
if not self.default_val:
|
|
||||||
return None
|
|
||||||
if not '...' in self.default_val:
|
|
||||||
return h2b(self.default_val)
|
|
||||||
l = self.default_val.split('...')
|
|
||||||
if len(l) != 2:
|
|
||||||
raise ValueError("Pattern '%s' contains more than one ..." % self.default_val)
|
|
||||||
prefix = h2b(l[0])
|
|
||||||
suffix = h2b(l[1])
|
|
||||||
pad_len = length - len(prefix) - len(suffix)
|
|
||||||
if pad_len <= 0:
|
|
||||||
ret = prefix + suffix
|
|
||||||
return ret[:length]
|
|
||||||
return prefix + prefix[-1:] * pad_len + suffix
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileTemplate:
|
|
||||||
"""Representation of a SimAlliance/TCA Profile Template. Each Template is identified by its OID and
|
|
||||||
consists of a number of file definitions. We implement each profile template as a class derived from this
|
|
||||||
base class. Each such derived class is a singleton and has no instances."""
|
|
||||||
created_by_default: bool = False
|
|
||||||
optional: bool = False
|
|
||||||
oid: Optional[OID.eOID] = None
|
|
||||||
files: List[FileTemplate] = []
|
|
||||||
|
|
||||||
# indicates that a given template does not have its own 'base DF', but that its contents merely
|
|
||||||
# extends that of the 'base DF' of another template
|
|
||||||
extends: Optional['ProfileTemplate'] = None
|
|
||||||
|
|
||||||
# indicates a parent ProfileTemplate below whose 'base DF' our files should be placed.
|
|
||||||
parent: Optional['ProfileTemplate'] = None
|
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
|
||||||
"""This classmethod is called automatically after executing the subclass body. We use it to
|
|
||||||
initialize the cls.files_by_pename from the cls.files"""
|
|
||||||
super().__init_subclass__(**kwargs)
|
|
||||||
cur_df = None
|
|
||||||
|
|
||||||
cls.files_by_pename: dict[str,FileTemplate] = {}
|
|
||||||
cls.tree: List[FileTemplate] = []
|
|
||||||
|
|
||||||
if not cls.optional and not cls.files[0].file_type in ['MF', 'DF', 'ADF']:
|
|
||||||
raise ValueError('First file in non-optional template must be MF, DF or ADF (is: %s)' % cls.files[0])
|
|
||||||
for f in cls.files:
|
|
||||||
if f.file_type in ['MF', 'DF', 'ADF']:
|
|
||||||
if cur_df == None:
|
|
||||||
cls.tree.append(f)
|
|
||||||
f.parent = None
|
|
||||||
cur_df = f
|
|
||||||
else:
|
|
||||||
# "cd .."
|
|
||||||
if cur_df.parent:
|
|
||||||
cur_df = cur_df.parent
|
|
||||||
f.parent = cur_df
|
|
||||||
cur_df.children.append(f)
|
|
||||||
cur_df = f
|
|
||||||
else:
|
|
||||||
if cur_df == None:
|
|
||||||
cls.tree.append(f)
|
|
||||||
f.parent = None
|
|
||||||
else:
|
|
||||||
cur_df.children.append(f)
|
|
||||||
f.parent = cur_df
|
|
||||||
cls.files_by_pename[f.pe_name] = f
|
|
||||||
ProfileTemplateRegistry.add(cls)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def print_tree(cls):
|
|
||||||
for c in cls.tree:
|
|
||||||
c.print_tree()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def base_df(cls) -> FileTemplate:
|
|
||||||
"""Return the FileTemplate for the base DF of the given template. This may be a DF or ADF
|
|
||||||
within this template, or refer to another template (e.g. mandatory USIM if we are optional USIM."""
|
|
||||||
if cls.extends:
|
|
||||||
return cls.extends.base_df
|
|
||||||
return cls.files[0]
|
|
||||||
|
|
||||||
class ProfileTemplateRegistry:
|
|
||||||
"""A registry of profile templates. Exists as a singleton class with no instances and only
|
|
||||||
classmethods."""
|
|
||||||
by_oid = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add(cls, tpl: ProfileTemplate):
|
|
||||||
"""Add a ProfileTemplate to the registry. There can only be one Template per OID."""
|
|
||||||
oid_str = str(tpl.oid)
|
|
||||||
if oid_str in cls.by_oid:
|
|
||||||
raise ValueError("We already have a template for OID %s" % oid_str)
|
|
||||||
cls.by_oid[oid_str] = tpl
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_oid(cls, oid: Union[List[int], str]) -> Optional[ProfileTemplate]:
|
|
||||||
"""Look-up the ProfileTemplate based on its OID. The OID can be given either in dotted-string format,
|
|
||||||
or as a list of integers."""
|
|
||||||
if not isinstance(oid, str):
|
|
||||||
oid = OID.OID.str_from_intlist(oid)
|
|
||||||
return cls.by_oid.get(oid, None)
|
|
||||||
|
|
||||||
# below are transcribed template definitions from "ANNEX A (Normative): File Structure Templates Definition"
|
|
||||||
# of "Profile interoperability specification V3.3.1 Final" (unless other version explicitly specified).
|
|
||||||
|
|
||||||
class FilesAtMF(ProfileTemplate):
|
|
||||||
"""Files at MF as per Section 9.2"""
|
|
||||||
created_by_default = True
|
|
||||||
oid = OID.MF
|
|
||||||
files = [
|
|
||||||
FileTemplate(0x3f00, 'MF', 'MF', None, None, 14, None, None, None, params=['pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x2f05, 'EF.PL', 'TR', None, 2, 1, 0x05, 'FF...FF', None),
|
|
||||||
FileTemplate(0x2f02, 'EF.ICCID', 'TR', None, 10, 11, None, None, True),
|
|
||||||
FileTemplate(0x2f00, 'EF.DIR', 'LF', None, None, 10, 0x1e, None, True, params=['nb_rec', 'size']),
|
|
||||||
FileTemplate(0x2f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, params=['nb_rec', 'size']),
|
|
||||||
FileTemplate(0x2f08, 'EF.UMPC', 'TR', None, 5, 10, 0x08, None, False),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class FilesCD(ProfileTemplate):
|
|
||||||
"""Files at DF.CD as per Section 9.3"""
|
|
||||||
created_by_default = False
|
|
||||||
oid = OID.DF_CD
|
|
||||||
files = [
|
|
||||||
FileTemplate(0x7f11, 'DF.CD', 'DF', None, None, 14, None, None, False, params=['pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x6f01, 'EF.LAUNCHPAD', 'TR', None, None, 2, None, None, True, params=['size']),
|
|
||||||
]
|
|
||||||
for i in range(0x40, 0x7f):
|
|
||||||
files.append(FileTemplate(0x6f00+i, 'EF.ICON', 'TR', None, None, 2, None, None, True, params=['size']))
|
|
||||||
|
|
||||||
|
|
||||||
# Section 9.4: Do this separately, so we can use them also from 9.5.3
|
|
||||||
df_pb_files = [
|
|
||||||
FileTemplate(0x5f3a, 'DF.PHONEBOOK', 'DF', None, None, 14, None, None, True, ['pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x4f30, 'EF.PBR', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size'], ppath=[0x5f3a]),
|
|
||||||
]
|
|
||||||
for i in range(0x38, 0x40):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EXT1', 'LF', None, 13, 5, None, '00FF...FF', False, ['size','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x40, 0x48):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.AAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x48, 0x50):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GAS', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ppath=[0x5f3a]))
|
|
||||||
df_pb_files += [
|
|
||||||
FileTemplate(0x4f22, 'EF.PSC', 'TR', None, 4, 5, None, '00000000', False, ['sfi'], ppath=[0x5f3a]),
|
|
||||||
FileTemplate(0x4f23, 'EF.CC', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True, ppath=[0x5f3a]),
|
|
||||||
FileTemplate(0x4f24, 'EF.PUID', 'TR', None, 2, 5, None, '0000', False, ['sfi'], high_update=True, ppath=[0x5f3a]),
|
|
||||||
]
|
|
||||||
for i in range(0x50, 0x58):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.IAP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x58, 0x60):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x60, 0x68):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ADN', 'LF', None, 2, 5, None, '00...00', False, ['nb_rec','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x68, 0x70):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.ANR', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x70, 0x78):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.PURI', 'LF', None, None, 5, None, None, True, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x78, 0x80):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.EMAIL', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x80, 0x88):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.SNE', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x88, 0x90):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.UID', 'LF', None, 2, 5, None, '0000', False, ['nb_rec','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x90, 0x98):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.GRP', 'LF', None, None, 5, None, '00...00', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
|
||||||
for i in range(0x98, 0xa0):
|
|
||||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
|
||||||
|
|
||||||
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(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
|
|
||||||
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
|
|
||||||
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
|
|
||||||
FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
|
|
||||||
FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
|
|
||||||
FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
|
|
||||||
FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size'], ppath=[0x5f50]),
|
|
||||||
# EF.IIDF below
|
|
||||||
FileTemplate(0x4f21, 'EF.ICE_GRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
|
|
||||||
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
|
|
||||||
# EF.ICON below
|
|
||||||
]
|
|
||||||
for i in range(0x40, 0x80):
|
|
||||||
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size'], ppath=[0x5f50]))
|
|
||||||
for i in range(0x80, 0xC0):
|
|
||||||
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'], ppath=[0x5f50]))
|
|
||||||
|
|
||||||
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
|
|
||||||
df_pb = deepcopy(df_pb_files)
|
|
||||||
files += df_pb
|
|
||||||
|
|
||||||
files += [
|
|
||||||
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
|
|
||||||
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
|
|
||||||
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
|
|
||||||
|
|
||||||
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], ppath=[0x5f3c]),
|
|
||||||
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size'], ppath=[0x5f3c]),
|
|
||||||
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True, ppath=[0x5f3c]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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(0x6f06, 'EF.ARR', 'LF', None, None, 10, None, None, True, ['nb_rec', 'size']),
|
|
||||||
FileTemplate(0x6f53, 'EF.RMA', 'LF', None, None, 3, None, None, True, ['nb_rec', 'size']),
|
|
||||||
FileTemplate(0x6f54, 'EF.SUME', 'TR', None, 22, 3, None, None, True),
|
|
||||||
FileTemplate(0x6fe0, 'EF.ICE_DN', 'LF', 50, 24, 9, None, 'FF...FF', False),
|
|
||||||
FileTemplate(0x6fe1, 'EF.ICE_FF', 'LF', None, None, 9, None, 'FF...FF', False, ['nb_rec', 'size']),
|
|
||||||
FileTemplate(0x6fe5, 'EF.PSISMSC', 'LF', None, None, 5, None, None, True, ['nb_rec', 'size'], ass_serv=[12,91]),
|
|
||||||
FileTemplate(0x5f50, 'DF.GRAPHICS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x4f20, 'EF.IMG', 'LF', None, None, 2, None, '00FF...FF', False, ['nb_rec', 'size'], ppath=[0x5f50]),
|
|
||||||
# EF.IIDF below
|
|
||||||
FileTemplate(0x4f21, 'EF.ICE_GRRAPHICS','BT',None,None, 9, None, None, False, ['size'], ppath=[0x5f50]),
|
|
||||||
FileTemplate(0x4f01, 'EF.LAUNCH_SCWS','TR',None, None, 10, None, None, True, ['size'], ppath=[0x5f50]),
|
|
||||||
# EF.ICON below
|
|
||||||
]
|
|
||||||
for i in range(0x40, 0x80):
|
|
||||||
files.append(FileTemplate(0x4f00+i, 'EF.IIDF', 'TR', None, None, 2, None, 'FF...FF', False, ['size'], ppath=[0x5f50]))
|
|
||||||
for i in range(0x80, 0xC0):
|
|
||||||
files.append(FileTemplate(0x4f00+i, 'EF.ICON', 'TR', None, None, 10, None, None, True, ['size'],ppath=[0x5f50]))
|
|
||||||
|
|
||||||
# we copy the objects (instances) here as we also use them below from FilesUsimDfPhonebook
|
|
||||||
df_pb = deepcopy(df_pb_files)
|
|
||||||
files += df_pb
|
|
||||||
|
|
||||||
files += [
|
|
||||||
FileTemplate(0x5f3b, 'DF.MULTIMEDIA','DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[67]),
|
|
||||||
FileTemplate(0x4f47, 'EF.MML', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
|
|
||||||
FileTemplate(0x4f48, 'EF.MMDF', 'BT', None, None, 5, None, None, False, ['size'], ass_serv=[67], ppath=[0x5f3b]),
|
|
||||||
|
|
||||||
FileTemplate(0x5f3c, 'DF.MMSS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x4f20, 'EF.MLPL', 'TR', None, None, 2, 0x01, None, True, ['size'], ppath=[0x5f3c]),
|
|
||||||
FileTemplate(0x4f21, 'EF.MSPL', 'TR', None, None, 2, 0x02, None, True, ['size'], ppath=[0x5f3c]),
|
|
||||||
FileTemplate(0x4f21, 'EF.MMSSMODE', 'TR', None, 1, 2, 0x03, None, True, ppath=[0x5f3c]),
|
|
||||||
|
|
||||||
FileTemplate(0x5f3d, 'DF.MCS', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv={'usim':109, 'isim': 15}),
|
|
||||||
FileTemplate(0x4f01, 'EF.MST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
|
|
||||||
FileTemplate(0x4f02, 'EF.MCSCONFIG', 'BT', None, None, 2, 0x02, None, True, ['size'], ass_serv={'usim':109, 'isim': 15}, ppath=[0x5f3d]),
|
|
||||||
|
|
||||||
FileTemplate(0x5f3e, 'DF.V2X', 'DF', None, None, 14, None, None, False, ['pinStatusTemplateDO'], ass_serv=[119]),
|
|
||||||
FileTemplate(0x4f01, 'EF.VST', 'TR', None, None, 2, 0x01, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]),
|
|
||||||
FileTemplate(0x4f02, 'EF.V2X_CONFIG','BT', None, None, 2, 0x02, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]),
|
|
||||||
FileTemplate(0x4f03, 'EF.V2XP_PC5', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]), # VST: 2
|
|
||||||
FileTemplate(0x4f04, 'EF.V2XP_Uu', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[119], ppath=[0x5f3e]), # VST: 3
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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 = [
|
|
||||||
FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
|
|
||||||
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
|
|
||||||
FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
|
|
||||||
FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name = 'ef-keysPS'),
|
|
||||||
FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
|
|
||||||
FileTemplate(0x6f38, 'EF.UST', 'TR', None, 14, 2, 0x04, None, True),
|
|
||||||
FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
|
|
||||||
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
|
|
||||||
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
|
|
||||||
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
|
|
||||||
FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
|
|
||||||
FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
|
|
||||||
FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
|
|
||||||
FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
|
|
||||||
FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
|
|
||||||
FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
|
|
||||||
FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
|
|
||||||
FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
|
|
||||||
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
|
|
||||||
FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
|
|
||||||
FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
|
|
||||||
FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
|
|
||||||
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
|
|
||||||
]
|
|
||||||
|
|
||||||
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 = [
|
|
||||||
FileTemplate( None, 'ADF.USIM', 'ADF', None, None, 14, None, None, False, ['aid', 'temp_fid', 'pinStatusTemplateDO']),
|
|
||||||
FileTemplate(0x6f07, 'EF.IMSI', 'TR', None, 9, 2, 0x07, None, True, ['size']),
|
|
||||||
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x17, None, True, ['nb_rec','size']),
|
|
||||||
FileTemplate(0x6f08, 'EF.Keys', 'TR', None, 33, 5, 0x08, '07FF...FF', False, high_update=True),
|
|
||||||
FileTemplate(0x6f09, 'EF.KeysPS', 'TR', None, 33, 5, 0x09, '07FF...FF', False, high_update=True, pe_name='ef-keysPS'),
|
|
||||||
FileTemplate(0x6f31, 'EF.HPPLMN', 'TR', None, 1, 2, 0x12, '0A', False),
|
|
||||||
FileTemplate(0x6f38, 'EF.UST', 'TR', None, 17, 2, 0x04, None, True),
|
|
||||||
FileTemplate(0x6f3b, 'EF.FDN', 'LF', 20, 26, 8, None, 'FF...FF', False, ass_serv=[2, 89]),
|
|
||||||
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[10]),
|
|
||||||
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[12]),
|
|
||||||
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[10]),
|
|
||||||
FileTemplate(0x6f46, 'EF.SPN', 'TR', None, 17, 10, None, None, True, ass_serv=[19]),
|
|
||||||
FileTemplate(0x6f56, 'EF.EST', 'TR', None, 1, 8, 0x05, None, True, ass_serv=[2,6,34,35]),
|
|
||||||
FileTemplate(0x6f5b, 'EF.START-HFN', 'TR', None, 6, 5, 0x0f, 'F00000F00000', False, high_update=True),
|
|
||||||
FileTemplate(0x6f5c, 'EF.THRESHOLD', 'TR', None, 3, 2, 0x10, 'FFFFFF', False),
|
|
||||||
FileTemplate(0x6f73, 'EF.PSLOCI', 'TR', None, 14, 5, 0x0c, 'FFFFFFFFFFFFFFFFFFFF0000FF01', False, high_update=True),
|
|
||||||
FileTemplate(0x6f78, 'EF.ACC', 'TR', None, 2, 2, 0x06, None, True),
|
|
||||||
FileTemplate(0x6f7b, 'EF.FPLMN', 'TR', None, 12, 5, 0x0d, 'FF...FF', False),
|
|
||||||
FileTemplate(0x6f7e, 'EF.LOCI', 'TR', None, 11, 5, 0x0b, 'FFFFFFFFFFFFFF0000FF01', False, high_update=True),
|
|
||||||
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 4, 10, 0x03, '00000002', False),
|
|
||||||
FileTemplate(0x6fb7, 'EF.ECC', 'LF', 1, 4, 10, 0x01, None, True),
|
|
||||||
FileTemplate(0x6fc4, 'EF.NETPAR', 'TR', None, 128, 5, None, 'FF...FF', False, high_update=True),
|
|
||||||
FileTemplate(0x6fe3, 'EF.EPSLOCI', 'TR', None, 18, 5, 0x1e, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000001', False, ass_serv=[85], high_update=True),
|
|
||||||
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
files = [
|
|
||||||
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
|
|
||||||
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13], pe_name='ef-acmax'),
|
|
||||||
FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
|
|
||||||
FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
|
|
||||||
FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
|
|
||||||
FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
|
|
||||||
FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
|
|
||||||
FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
|
|
||||||
FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
|
|
||||||
FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
|
|
||||||
FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
|
|
||||||
FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
|
|
||||||
FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
|
|
||||||
FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000', False, ass_serv=[20], repeat=True),
|
|
||||||
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000', False, ass_serv=[42], repeat=True),
|
|
||||||
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43], repeat=True),
|
|
||||||
FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
|
|
||||||
FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
|
|
||||||
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
|
|
||||||
FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
|
|
||||||
FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
|
|
||||||
FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
|
|
||||||
FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
|
|
||||||
FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
|
|
||||||
FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
|
|
||||||
FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
|
|
||||||
FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
|
|
||||||
FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
|
|
||||||
FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
|
|
||||||
FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
|
|
||||||
FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
|
|
||||||
FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
|
|
||||||
FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
|
|
||||||
FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
|
|
||||||
FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
|
|
||||||
FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
|
|
||||||
FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
|
|
||||||
FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
|
|
||||||
FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
|
|
||||||
FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
|
|
||||||
FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
|
|
||||||
FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
|
|
||||||
FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
|
|
||||||
FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
|
|
||||||
FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
|
|
||||||
FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
|
|
||||||
FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
|
|
||||||
FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
|
|
||||||
FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
|
|
||||||
FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
|
|
||||||
FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
|
|
||||||
FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
|
|
||||||
FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
|
|
||||||
FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
|
|
||||||
FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
|
|
||||||
FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
|
|
||||||
FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
|
|
||||||
FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
|
|
||||||
FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
|
|
||||||
FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
|
|
||||||
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
|
|
||||||
FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
|
|
||||||
FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
|
|
||||||
FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
|
|
||||||
FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
|
|
||||||
FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
|
|
||||||
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
|
|
||||||
FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
|
|
||||||
FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
|
|
||||||
FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
|
|
||||||
FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
|
|
||||||
FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
|
|
||||||
FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
|
|
||||||
FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Section 9.5.2
|
|
||||||
class FilesUsimOptionalV2(ProfileTemplate):
|
|
||||||
"""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
|
|
||||||
files = [
|
|
||||||
FileTemplate(0x6f05, 'EF.LI', 'TR', None, 6, 1, 0x02, 'FF...FF', False),
|
|
||||||
FileTemplate(0x6f37, 'EF.ACMmax', 'TR', None, 3, 5, None, '000000', False, ass_serv=[13]),
|
|
||||||
FileTemplate(0x6f39, 'EF.ACM', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[13], high_update=True),
|
|
||||||
FileTemplate(0x6f3e, 'EF.GID1', 'TR', None, 8, 2, None, None, True, ass_serv=[17]),
|
|
||||||
FileTemplate(0x6f3f, 'EF.GID2', 'TR', None, 8, 2, None, None, True, ass_serv=[18]),
|
|
||||||
FileTemplate(0x6f40, 'EF.MSISDN', 'LF', 1, 24, 2, None, 'FF...FF', False, ass_serv=[21]),
|
|
||||||
FileTemplate(0x6f41, 'EF.PUCT', 'TR', None, 5, 5, None, 'FFFFFF0000', False, ass_serv=[13]),
|
|
||||||
FileTemplate(0x6f45, 'EF.CBMI', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[15]),
|
|
||||||
FileTemplate(0x6f48, 'EF.CBMID', 'TR', None, 10, 2, 0x0e, 'FF...FF', False, ass_serv=[19]),
|
|
||||||
FileTemplate(0x6f49, 'EF.SDN', 'LF', 10, 24, 2, None, 'FF...FF', False, ass_serv=[4,89]),
|
|
||||||
FileTemplate(0x6f4b, 'EF.EXT2', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[3]),
|
|
||||||
FileTemplate(0x6f4c, 'EF.EXT3', 'LF', 10, 13, 2, None, '00FF...FF', False, ass_serv=[5]),
|
|
||||||
FileTemplate(0x6f50, 'EF.CBMIR', 'TR', None, 20, 5, None, 'FF...FF', False, ass_serv=[16]),
|
|
||||||
FileTemplate(0x6f60, 'EF.PLMNwAcT', 'TR', None, 40, 5, 0x0a, 'FFFFFF0000'*8, False, ass_serv=[20]),
|
|
||||||
FileTemplate(0x6f61, 'EF.OPLMNwAcT', 'TR', None, 40, 2, 0x11, 'FFFFFF0000'*8, False, ass_serv=[42]),
|
|
||||||
FileTemplate(0x6f62, 'EF.HPLMNwAcT', 'TR', None, 5, 2, 0x13, 'FFFFFF0000', False, ass_serv=[43]),
|
|
||||||
FileTemplate(0x6f2c, 'EF.DCK', 'TR', None, 16, 5, None, 'FF...FF', False, ass_serv=[36]),
|
|
||||||
FileTemplate(0x6f32, 'EF.CNL', 'TR', None, 30, 2, None, 'FF...FF', False, ass_serv=[37]),
|
|
||||||
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[11]),
|
|
||||||
FileTemplate(0x6f4d, 'EF.BDN', 'LF', 10, 25, 8, None, 'FF...FF', False, ass_serv=[6]),
|
|
||||||
FileTemplate(0x6f4e, 'EF.EXT5', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[44]),
|
|
||||||
FileTemplate(0x6f4f, 'EF.CCP2', 'LF', 5, 15, 5, 0x16, 'FF...FF', False, ass_serv=[14]),
|
|
||||||
FileTemplate(0x6f55, 'EF.EXT4', 'LF', 10, 13, 8, None, '00FF...FF', False, ass_serv=[7]),
|
|
||||||
FileTemplate(0x6f57, 'EF.ACL', 'TR', None, 101, 8, None, '00FF...FF', False, ass_serv=[35]),
|
|
||||||
FileTemplate(0x6f58, 'EF.CMI', 'LF', 10, 11, 2, None, 'FF...FF', False, ass_serv=[6]),
|
|
||||||
FileTemplate(0x6f80, 'EF.ICI', 'CY', 20, 38, 5, 0x14, 'FF...FF0000000001FFFF', False, ass_serv=[9], high_update=True),
|
|
||||||
FileTemplate(0x6f81, 'EF.OCI', 'CY', 20, 37, 5, 0x15, 'FF...FF00000001FFFF', False, ass_serv=[8], high_update=True),
|
|
||||||
FileTemplate(0x6f82, 'EF.ICT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[9], high_update=True),
|
|
||||||
FileTemplate(0x6f83, 'EF.OCT', 'CY', 1, 3, 7, None, '000000', False, ass_serv=[8], high_update=True),
|
|
||||||
FileTemplate(0x6fb1, 'EF.VGCS', 'TR', None, 20, 2, None, None, True, ass_serv=[57]),
|
|
||||||
FileTemplate(0x6fb2, 'EF.VGCSS', 'TR', None, 7, 5, None, None, True, ass_serv=[57]),
|
|
||||||
FileTemplate(0x6fb3, 'EF.VBS', 'TR', None, 20, 2, None, None, True, ass_serv=[58]),
|
|
||||||
FileTemplate(0x6fb4, 'EF.VBSS', 'TR', None, 7, 5, None, None, True, ass_serv=[58]), # ARR 2!??
|
|
||||||
FileTemplate(0x6fb5, 'EF.eMLPP', 'TR', None, 2, 2, None, None, True, ass_serv=[24]),
|
|
||||||
FileTemplate(0x6fb6, 'EF.AaeM', 'TR', None, 1, 5, None, '00', False, ass_serv=[25]),
|
|
||||||
FileTemplate(0x6fc3, 'EF.HiddenKey', 'TR', None, 4, 5, None, 'FF...FF', False),
|
|
||||||
FileTemplate(0x6fc5, 'EF.PNN', 'LF', 10, 16, 10, 0x19, None, True, ass_serv=[45]),
|
|
||||||
FileTemplate(0x6fc6, 'EF.OPL', 'LF', 5, 8, 10, 0x1a, None, True, ass_serv=[46]),
|
|
||||||
FileTemplate(0x6fc7, 'EF.MBDN', 'LF', 3, 24, 5, None, None, True, ass_serv=[47]),
|
|
||||||
FileTemplate(0x6fc8, 'EF.EXT6', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[47]),
|
|
||||||
FileTemplate(0x6fc9, 'EF.MBI', 'LF', 10, 5, 5, None, None, True, ass_serv=[47]),
|
|
||||||
FileTemplate(0x6fca, 'EF.MWIS', 'LF', 10, 6, 5, None, '00...00', False, ass_serv=[48], high_update=True),
|
|
||||||
FileTemplate(0x6fcb, 'EF.CFIS', 'LF', 10, 16, 5, None, '0100FF...FF', False, ass_serv=[49]),
|
|
||||||
FileTemplate(0x6fcb, 'EF.EXT7', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[49]),
|
|
||||||
FileTemplate(0x6fcd, 'EF.SPDI', 'TR', None, 17, 2, 0x1b, None, True, ass_serv=[51]),
|
|
||||||
FileTemplate(0x6fce, 'EF.MMSN', 'LF', 10, 6, 5, None, '000000FF...FF', False, ass_serv=[52]),
|
|
||||||
FileTemplate(0x6fcf, 'EF.EXT8', 'LF', 10, 13, 5, None, '00FF...FF', False, ass_serv=[53]),
|
|
||||||
FileTemplate(0x6fd0, 'EF.MMSICP', 'TR', None, 100, 2, None, 'FF...FF', False, ass_serv=[52]),
|
|
||||||
FileTemplate(0x6fd1, 'EF.MMSUP', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[52]),
|
|
||||||
FileTemplate(0x6fd2, 'EF.MMSUCP', 'TR', None, 100, 5, None, 'FF...FF', False, ass_serv=[52,55]),
|
|
||||||
FileTemplate(0x6fd3, 'EF.NIA', 'LF', 5, 11, 2, None, 'FF...FF', False, ass_serv=[56]),
|
|
||||||
FileTemplate(0x6fd4, 'EF.VGCSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[64]),
|
|
||||||
FileTemplate(0x6fd5, 'EF.VBSCA', 'TR', None, None, 2, None, '00...00', False, ['size'], ass_serv=[65]),
|
|
||||||
FileTemplate(0x6fd6, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[68]),
|
|
||||||
FileTemplate(0x6fd7, 'EF.MSK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69], high_update=True),
|
|
||||||
FileTemplate(0x6fd8, 'EF.MUK', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[69]),
|
|
||||||
FileTemplate(0x6fd9, 'EF.EHPLMN', 'TR', None, 15, 2, 0x1d, 'FF...FF', False, ass_serv=[71]),
|
|
||||||
FileTemplate(0x6fda, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68]),
|
|
||||||
FileTemplate(0x6fdb, 'EF.EHPLMNPI', 'TR', None, 1, 2, None, '00', False, ass_serv=[71,73]),
|
|
||||||
FileTemplate(0x6fdc, 'EF.LRPLMNSI', 'TR', None, 1, 2, None, '00', False, ass_serv=[74]),
|
|
||||||
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[68,76]),
|
|
||||||
FileTemplate(0x6fde, 'EF.SPNI', 'TR', None, None, 10, None, '00FF...FF', False, ['size'], ass_serv=[78]),
|
|
||||||
FileTemplate(0x6fdf, 'EF.PNNI', 'LF', None, None, 10, None, '00FF...FF', False, ['nb_rec','size'], ass_serv=[79]),
|
|
||||||
FileTemplate(0x6fe2, 'EF.NCP-IP', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[80]),
|
|
||||||
FileTemplate(0x6fe6, 'EF.UFC', 'TR', None, 30, 10, None, '801E60C01E900080040000000000000000F0000000004000000000000080', False),
|
|
||||||
FileTemplate(0x6fe8, 'EF.NASCONFIG', 'TR', None, 18, 2, None, None, True, ass_serv=[96]),
|
|
||||||
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[95]),
|
|
||||||
FileTemplate(0x6fec, 'EF.PWS', 'TR', None, None, 10, None, None, True, ['size'], ass_serv=[97]),
|
|
||||||
FileTemplate(0x6fed, 'EF.FDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,99]),
|
|
||||||
FileTemplate(0x6fee, 'EF.BDNURI', 'LF', None, None, 8, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[6,99]),
|
|
||||||
FileTemplate(0x6fef, 'EF.SDNURI', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[4,99]),
|
|
||||||
FileTemplate(0x6ff0, 'EF.IWL', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102]),
|
|
||||||
FileTemplate(0x6ff1, 'EF.IPS', 'CY', None, 4, 10, None, 'FF...FF', False, ['size'], ass_serv=[102], high_update=True),
|
|
||||||
FileTemplate(0x6ff2, 'EF.IPD', 'LF', None, None, 3, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[102], high_update=True),
|
|
||||||
FileTemplate(0x6ff3, 'EF.EPDGID', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
|
|
||||||
FileTemplate(0x6ff4, 'EF.EPDGSELECTION','TR',None,None, 2, None, None, True, ['size'], ass_serv=[(106, 107)]),
|
|
||||||
FileTemplate(0x6ff5, 'EF.EPDGIDEM', 'TR', None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
|
|
||||||
FileTemplate(0x6ff6, 'EF.EPDGIDEMSEL','TR',None, None, 2, None, None, True, ['size'], ass_serv=[(110, 111)]),
|
|
||||||
FileTemplate(0x6ff7, 'EF.FromPreferred','TR',None, 1, 2, None, '00', False, ass_serv=[114]),
|
|
||||||
FileTemplate(0x6ff8, 'EF.IMSConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[115]),
|
|
||||||
FileTemplate(0x6ff9, 'EF.3GPPPSDataOff','TR',None, 4, 2, None, None, True, ass_serv=[117]),
|
|
||||||
FileTemplate(0x6ffa, 'EF.3GPPPSDOSLIST','LF',None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[118]),
|
|
||||||
FileTemplate(0x6ffc, 'EF.XCAPConfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[120]),
|
|
||||||
FileTemplate(0x6ffd, 'EF.EARFCNLIST','TR', None, None, 10, None, None, True, ['size'], ass_serv=[121]),
|
|
||||||
FileTemplate(0x6ffd, 'EF.MudMidCfgdata','BT', None, None,2, None, None, True, ['size'], ass_serv=[134]),
|
|
||||||
]
|
|
||||||
|
|
||||||
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]),
|
|
||||||
]
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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),
|
|
||||||
FileTemplate(0x4f52, 'EF.KcGPRS', 'TR', None, 9, 5, 0x02, 'FF...FF07', False, ass_serv=[27], high_update=True),
|
|
||||||
FileTemplate(0x4f63, 'EF.CPBCCH', 'TR', None, 10, 5, None, 'FF...FF', False, ass_serv=[39], high_update=True),
|
|
||||||
FileTemplate(0x4f64, 'EF.InvScan', 'TR', None, 1, 2, None, '00', False, ass_serv=[40]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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),
|
|
||||||
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
|
||||||
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
|
|
||||||
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
|
|
||||||
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
|
|
||||||
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
|
|
||||||
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
|
|
||||||
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
|
|
||||||
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
|
|
||||||
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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),
|
|
||||||
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
|
||||||
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 1, 57, 5, 0x03, 'FF...FF', False, ass_serv=[122], high_update=True),
|
|
||||||
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 1, 57, 5, 0x04, 'FF...FF', False, ass_serv=[122], high_update=True),
|
|
||||||
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
|
|
||||||
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
|
|
||||||
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
|
|
||||||
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
|
|
||||||
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130]),
|
|
||||||
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
|
|
||||||
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
|
|
||||||
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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),
|
|
||||||
FileTemplate(0x4f02, 'EF.5GSN3GPPLOCI', 'TR', None, 20, 5, 0x02, 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000001', False, ass_serv=[122], high_update=True),
|
|
||||||
FileTemplate(0x4f03, 'EF.5GS3GPPNSC', 'LF', 2, 62, 5, 0x03, 'FF...FF', False, ass_serv=[122,136], high_update=True),
|
|
||||||
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
|
|
||||||
FileTemplate(0x4f04, 'EF.5GSN3GPPNSC', 'LF', 2, 62, 5, 0x04, 'FF...FF', False, ass_serv=[122,136], high_update=True),
|
|
||||||
# ^ If Service n°136 is not "available" in EF UST, the Profile Creator shall ensure that these files shall contain one record; otherwise, they shall contain 2 records.
|
|
||||||
FileTemplate(0x4f05, 'EF.5GAUTHKEYS', 'TR', None, 110, 5, 0x05, None, True, ass_serv=[123], high_update=True),
|
|
||||||
FileTemplate(0x4f06, 'EF.UAC_AIC', 'TR', None, 4, 2, 0x06, None, True, ass_serv=[126]),
|
|
||||||
FileTemplate(0x4f07, 'EF.SUCI_Calc_Info', 'TR', None, None, 2, 0x07, 'FF...FF', False, ass_serv=[124]),
|
|
||||||
FileTemplate(0x4f08, 'EF.OPL5G', 'LF', None, 10, 10, 0x08, 'FF...FF', False, ['nb_rec'], ass_serv=[129]),
|
|
||||||
FileTemplate(0x4f09, 'EF.SUPI_NAI', 'TR', None, None, 2, 0x09, None, True, ['size'], ass_serv=[130], pe_name='ef-supinai'),
|
|
||||||
FileTemplate(0x4f0a, 'EF.Routing_Indicator', 'TR', None, 4, 2, 0x0a, 'F0FFFFFF', False, ass_serv=[124]),
|
|
||||||
FileTemplate(0x4f0b, 'EF.URSP', 'BT', None, None, 2, None, None, False, ass_serv=[132]),
|
|
||||||
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
|
|
||||||
]
|
|
||||||
|
|
||||||
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]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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'),
|
|
||||||
]
|
|
||||||
|
|
||||||
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]),
|
|
||||||
]
|
|
||||||
|
|
||||||
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(0x6f02, 'EF.IMPI', 'TR', None, None, 2, 0x02, None, True, ['size']),
|
|
||||||
FileTemplate(0x6f04, 'EF.IMPU', 'LF', 1, None, 2, 0x04, None, True, ['size']),
|
|
||||||
FileTemplate(0x6f03, 'EF.Domain', 'TR', None, None, 2, 0x05, None, True, ['size']),
|
|
||||||
FileTemplate(0x6f07, 'EF.IST', 'TR', None, 14, 2, 0x07, None, True),
|
|
||||||
FileTemplate(0x6fad, 'EF.AD', 'TR', None, 3, 10, 0x03, '000000', False),
|
|
||||||
FileTemplate(0x6f06, 'EF.ARR', 'LF', None, None, 10, 0x06, None, True, ['nb_rec','size']),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
files = [
|
|
||||||
FileTemplate(0x6f09, 'EF.P-CSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
|
|
||||||
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
|
|
||||||
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
|
|
||||||
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
|
|
||||||
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
|
|
||||||
FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
|
|
||||||
FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
|
|
||||||
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
|
|
||||||
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
files = [
|
|
||||||
FileTemplate(0x6f09, 'EF.PCSCF', 'LF', 1, None, 2, None, None, True, ['size'], ass_serv=[1,5]),
|
|
||||||
FileTemplate(0x6f3c, 'EF.SMS', 'LF', 10, 176, 5, None, '00FF...FF', False, ass_serv=[6,8]),
|
|
||||||
FileTemplate(0x6f42, 'EF.SMSP', 'LF', 1, 38, 5, None, 'FF...FF', False, ass_serv=[8]),
|
|
||||||
FileTemplate(0x6f43, 'EF.SMSS', 'TR', None, 2, 5, None, 'FFFF', False, ass_serv=[6,8]),
|
|
||||||
FileTemplate(0x6f47, 'EF.SMSR', 'LF', 10, 30, 5, None, '00FF...FF', False, ass_serv=[7,8]),
|
|
||||||
FileTemplate(0x6fd5, 'EF.GBABP', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], ass_serv=[2]),
|
|
||||||
FileTemplate(0x6fd7, 'EF.GBANL', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2]),
|
|
||||||
FileTemplate(0x6fdd, 'EF.NAFKCA', 'LF', None, None, 2, None, 'FF...FF', False, ['nb_rec','size'], ass_serv=[2,4]),
|
|
||||||
FileTemplate(0x6fe7, 'EF.UICCIARI', 'LF', None, None, 2, None, None, True, ['nb_rec','size'], ass_serv=[10]),
|
|
||||||
FileTemplate(0x6ff7, 'EF.FromPreferred','TR', None, 1, 2, None, '00', False, ass_serv=[17]),
|
|
||||||
FileTemplate(0x6ff8, 'EF.ImsConfigData','BT', None,None, 2, None, None, True, ['size'], ass_serv=[18]),
|
|
||||||
FileTemplate(0x6ffc, 'EF.XcapconfigData','BT',None,None, 2, None, None, True, ['size'], ass_serv=[19]),
|
|
||||||
FileTemplate(0x6ffa, 'EF.WebRTCURI', 'LF', None, None, 2, None, None, True, ['nb_rec', 'size'], ass_serv=[20]),
|
|
||||||
FileTemplate(0x6ffa, 'EF.MudMidCfgData','BT',None, None, 2, None, None, True, ['size'], ass_serv=[21]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: CSIM
|
|
||||||
|
|
||||||
|
|
||||||
class FilesEap(ProfileTemplate):
|
|
||||||
"""Files at DF.EAP as per Section 9.8"""
|
|
||||||
created_by_default = False
|
|
||||||
oid = OID.DF_EAP
|
|
||||||
files = [
|
|
||||||
FileTemplate( None, 'DF.EAP', 'DF', None, None, 14, None, None, False, ['fid','pinStatusTemplateDO'], ass_serv=[(124, 125)]),
|
|
||||||
FileTemplate(0x4f01, 'EF.EAPKEYS', 'TR', None, None, 2, None, None, True, ['size'], high_update=True),
|
|
||||||
FileTemplate(0x4f02, 'EF.EAPSTATUS', 'TR', None, 1, 2, None, '00', False, high_update=True),
|
|
||||||
FileTemplate(0x4f03, 'EF.PUId', 'TR', None, None, 2, None, None, True, ['size']),
|
|
||||||
FileTemplate(0x4f04, 'EF.Ps', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], high_update=True),
|
|
||||||
FileTemplate(0x4f20, 'EF.CurID', 'TR', None, None, 5, None, 'FF...FF', False, ['size'], high_update=True),
|
|
||||||
FileTemplate(0x4f21, 'EF.RelID', 'TR', None, None, 5, None, None, True, ['size']),
|
|
||||||
FileTemplate(0x4f22, 'EF.Realm', 'TR', None, None, 5, None, None, True, ['size']),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Section 9.9 Access Rules Definition
|
|
||||||
ARR_DEFINITION = {
|
|
||||||
1: ['8001019000', '800102A406830101950108', '800158A40683010A950108'],
|
|
||||||
2: ['800101A406830101950108', '80015AA40683010A950108'],
|
|
||||||
3: ['80015BA40683010A950108'],
|
|
||||||
4: ['8001019000', '80011A9700', '800140A40683010A950108'],
|
|
||||||
5: ['800103A406830101950108', '800158A40683010A950108'],
|
|
||||||
6: ['800111A406830101950108', '80014AA40683010A950108'],
|
|
||||||
7: ['800103A406830101950108', '800158A40683010A950108', '840132A406830101950108'],
|
|
||||||
8: ['800101A406830101950108', '800102A406830181950108', '800158A40683010A950108'],
|
|
||||||
9: ['8001019000', '80011AA406830101950108', '800140A40683010A950108'],
|
|
||||||
10: ['8001019000', '80015AA40683010A950108'],
|
|
||||||
11: ['8001019000', '800118A40683010A950108', '8001429700'],
|
|
||||||
12: ['800101A406830101950108', '80015A9700'],
|
|
||||||
13: ['800113A406830101950108', '800148A40683010A950108'],
|
|
||||||
14: ['80015EA40683010A950108'],
|
|
||||||
}
|
|
||||||
|
|
||||||
class SaipSpecVersionMeta(type):
|
|
||||||
def __getitem__(self, ver: str):
|
|
||||||
"""Syntactic sugar so that SaipSpecVersion['2.3.0'] will work."""
|
|
||||||
return SaipSpecVersion.for_version(ver)
|
|
||||||
|
|
||||||
class SaipSpecVersion(object, metaclass=SaipSpecVersionMeta):
|
|
||||||
"""Represents a specific version of the SIMalliance / TCA eUICC Profile Package:
|
|
||||||
Interoperable Format Technical Specification."""
|
|
||||||
version = None
|
|
||||||
oids = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suports_template_OID(cls, OID: OID.OID) -> bool:
|
|
||||||
"""Return if a given spec version supports a template of given OID."""
|
|
||||||
return OID in cls.oids
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def version_match(cls, ver: str) -> bool:
|
|
||||||
"""Check if the given version-string matches the classes version. trailing zeroes are ignored,
|
|
||||||
so that for example 2.2.0 will be considered equal to 2.2"""
|
|
||||||
def strip_trailing_zeroes(l: List):
|
|
||||||
while l[-1] == '0':
|
|
||||||
l.pop()
|
|
||||||
cls_ver_l = cls.version.split('.')
|
|
||||||
strip_trailing_zeroes(cls_ver_l)
|
|
||||||
ver_l = ver.split('.')
|
|
||||||
strip_trailing_zeroes(ver_l)
|
|
||||||
return cls_ver_l == ver_l
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def for_version(req_version: str) -> Optional['SaipSpecVersion']:
|
|
||||||
"""Return the subclass for the requested version number string."""
|
|
||||||
for cls in all_subclasses(SaipSpecVersion):
|
|
||||||
if cls.version_match(req_version):
|
|
||||||
return cls
|
|
||||||
|
|
||||||
|
|
||||||
class SaipSpecVersion101(SaipSpecVersion):
|
|
||||||
version = '1.0.1'
|
|
||||||
oids = [OID.MF, OID.DF_CD, OID.DF_TELECOM, OID.ADF_USIM_by_default, OID.ADF_USIMopt_not_by_default,
|
|
||||||
OID.DF_PHONEBOOK_ADF_USIM, OID.DF_GSM_ACCESS_ADF_USIM, OID.ADF_ISIM_by_default,
|
|
||||||
OID.ADF_ISIMopt_not_by_default, OID.ADF_CSIM_by_default, OID.ADF_CSIMopt_not_by_default]
|
|
||||||
|
|
||||||
class SaipSpecVersion20(SaipSpecVersion):
|
|
||||||
version = '2.0'
|
|
||||||
# no changes in filesystem teplates to previous 1.0.1
|
|
||||||
oids = SaipSpecVersion101.oids
|
|
||||||
|
|
||||||
class SaipSpecVersion21(SaipSpecVersion):
|
|
||||||
version = '2.1'
|
|
||||||
# no changes in filesystem teplates to previous 2.0
|
|
||||||
oids = SaipSpecVersion20.oids
|
|
||||||
|
|
||||||
class SaipSpecVersion22(SaipSpecVersion):
|
|
||||||
version = '2.2'
|
|
||||||
oids = SaipSpecVersion21.oids + [OID.DF_EAP]
|
|
||||||
|
|
||||||
class SaipSpecVersion23(SaipSpecVersion):
|
|
||||||
version = '2.3'
|
|
||||||
oids = SaipSpecVersion22.oids + [OID.DF_5GS, OID.DF_SAIP]
|
|
||||||
|
|
||||||
class SaipSpecVersion231(SaipSpecVersion):
|
|
||||||
version = '2.3.1'
|
|
||||||
# no changes in filesystem teplates to previous 2.3
|
|
||||||
oids = SaipSpecVersion23.oids
|
|
||||||
|
|
||||||
class SaipSpecVersion31(SaipSpecVersion):
|
|
||||||
version = '3.1'
|
|
||||||
oids = [OID.MF, OID.DF_CD, OID.DF_TELECOM_v2, OID.ADF_USIM_by_default_v2, OID.ADF_USIMopt_not_by_default_v2,
|
|
||||||
OID.DF_PHONEBOOK_ADF_USIM, OID.DF_GSM_ACCESS_ADF_USIM, OID.DF_5GS_v2, OID.DF_5GS_v3, OID.DF_SAIP,
|
|
||||||
OID.ADF_ISIM_by_default, OID.ADF_ISIMopt_not_by_default_v2, OID.ADF_CSIM_by_default_v2,
|
|
||||||
OID.ADF_CSIMopt_not_by_default_v2, OID.DF_EAP]
|
|
||||||
|
|
||||||
class SaipSpecVersion32(SaipSpecVersion):
|
|
||||||
version = '3.2'
|
|
||||||
# no changes in filesystem teplates to previous 3.1
|
|
||||||
oids = SaipSpecVersion31.oids
|
|
||||||
|
|
||||||
class SaipSpecVersion331(SaipSpecVersion):
|
|
||||||
version = '3.3.1'
|
|
||||||
oids = SaipSpecVersion32.oids + [OID.ADF_USIMopt_not_by_default_v3, OID.DF_5GS_v4, OID.DF_SAIP, OID.DF_SNPN, OID.DF_5GProSe, OID.IoT_by_default, OID.IoTopt_not_by_default]
|
|
||||||
@@ -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>
|
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
@@ -19,21 +19,16 @@
|
|||||||
from pySim.esim.saip import *
|
from pySim.esim.saip import *
|
||||||
|
|
||||||
class ProfileError(Exception):
|
class ProfileError(Exception):
|
||||||
"""Raised when a ProfileConstraintChecker finds an error in a file [structure]."""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ProfileConstraintChecker:
|
class ProfileConstraintChecker:
|
||||||
"""Base class of a constraint checker for a ProfileElementSequence."""
|
|
||||||
def check(self, pes: ProfileElementSequence):
|
def check(self, pes: ProfileElementSequence):
|
||||||
"""Execute all the check_* methods of the ProfileConstraintChecker against the given
|
|
||||||
ProfileElementSequence"""
|
|
||||||
for name in dir(self):
|
for name in dir(self):
|
||||||
if name.startswith('check_'):
|
if name.startswith('check_'):
|
||||||
method = getattr(self, name)
|
method = getattr(self, name)
|
||||||
method(pes)
|
method(pes)
|
||||||
|
|
||||||
class CheckBasicStructure(ProfileConstraintChecker):
|
class CheckBasicStructure(ProfileConstraintChecker):
|
||||||
"""ProfileConstraintChecker for the basic profile structure constraints."""
|
|
||||||
def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
|
def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
|
||||||
opt_pe = pes.get_pe_for_type(opt)
|
opt_pe = pes.get_pe_for_type(opt)
|
||||||
if opt_pe:
|
if opt_pe:
|
||||||
@@ -43,7 +38,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
|||||||
# FIXME: check order
|
# FIXME: check order
|
||||||
|
|
||||||
def check_start_and_end(self, pes: ProfileElementSequence):
|
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':
|
if pes.pe_list[0].type != 'header':
|
||||||
raise ProfileError('first element is not header')
|
raise ProfileError('first element is not header')
|
||||||
if pes.pe_list[1].type != 'mf':
|
if pes.pe_list[1].type != 'mf':
|
||||||
@@ -53,7 +47,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
|||||||
raise ProfileError('last element is not end')
|
raise ProfileError('last element is not end')
|
||||||
|
|
||||||
def check_number_of_occurrence(self, pes: ProfileElementSequence):
|
def check_number_of_occurrence(self, pes: ProfileElementSequence):
|
||||||
"""Check The number of occurrence of various ProfileElements."""
|
|
||||||
# check for invalid number of occurrences
|
# check for invalid number of occurrences
|
||||||
if len(pes.get_pes_for_type('header')) != 1:
|
if len(pes.get_pes_for_type('header')) != 1:
|
||||||
raise ProfileError('multiple ProfileHeader')
|
raise ProfileError('multiple ProfileHeader')
|
||||||
@@ -67,8 +60,7 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
|||||||
raise ProfileError('multiple PE-%s' % tn.upper())
|
raise ProfileError('multiple PE-%s' % tn.upper())
|
||||||
|
|
||||||
def check_optional_ordering(self, pes: ProfileElementSequence):
|
def check_optional_ordering(self, pes: ProfileElementSequence):
|
||||||
"""Check the ordering of optional PEs following the respective mandatory ones."""
|
# ordering and required depenencies
|
||||||
# ordering and required dependencies
|
|
||||||
self._is_after_if_exists(pes,'opt-usim', 'usim')
|
self._is_after_if_exists(pes,'opt-usim', 'usim')
|
||||||
self._is_after_if_exists(pes,'opt-isim', 'isim')
|
self._is_after_if_exists(pes,'opt-isim', 'isim')
|
||||||
self._is_after_if_exists(pes,'gsm-access', 'usim')
|
self._is_after_if_exists(pes,'gsm-access', 'usim')
|
||||||
@@ -100,47 +92,5 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
|||||||
raise ProfileError('get-identity mandatory, but no usim or isim')
|
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):
|
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')
|
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')
|
raise ProfileError('profile-a-p256 mandatory, but no usim or isim')
|
||||||
|
|
||||||
def check_identification_unique(self, pes: ProfileElementSequence):
|
|
||||||
"""Ensure that each PE has a unique identification value."""
|
|
||||||
id_list = [pe.header['identification'] for pe in pes.pe_list if pe.header]
|
|
||||||
if len(id_list) != len(set(id_list)):
|
|
||||||
raise ProfileError('PE identification values are not unique')
|
|
||||||
|
|
||||||
FileChoiceList = List[Tuple]
|
|
||||||
|
|
||||||
class FileError(ProfileError):
|
|
||||||
"""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."""
|
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
"""Implementation of X.509 certificate handling in GSMA eSIM as per SGP22 v3.0"""
|
|
||||||
|
|
||||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
|
||||||
from cryptography.hazmat.primitives import hashes
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding
|
|
||||||
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
|
|
||||||
|
|
||||||
from pySim.utils import b2h
|
|
||||||
|
|
||||||
def check_signed(signed: x509.Certificate, signer: x509.Certificate) -> bool:
|
|
||||||
"""Verify if 'signed' certificate was signed using 'signer'."""
|
|
||||||
# this code only works for ECDSA, but this is all we need for GSMA eSIM
|
|
||||||
pkey = signer.public_key()
|
|
||||||
# this 'signed.signature_algorithm_parameters' below requires cryptography 41.0.0 :(
|
|
||||||
pkey.verify(signed.signature, signed.tbs_certificate_bytes, signed.signature_algorithm_parameters)
|
|
||||||
|
|
||||||
def cert_get_subject_key_id(cert: x509.Certificate) -> bytes:
|
|
||||||
"""Obtain the subject key identifier of the given cert object (as raw bytes)."""
|
|
||||||
ski_ext = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
|
|
||||||
return ski_ext.key_identifier
|
|
||||||
|
|
||||||
def cert_get_auth_key_id(cert: x509.Certificate) -> bytes:
|
|
||||||
"""Obtain the authority key identifier of the given cert object (as raw bytes)."""
|
|
||||||
aki_ext = cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value
|
|
||||||
return aki_ext.key_identifier
|
|
||||||
|
|
||||||
def cert_policy_has_oid(cert: x509.Certificate, match_oid: x509.ObjectIdentifier) -> bool:
|
|
||||||
"""Determine if given certificate has a certificatePolicy extension of matching OID."""
|
|
||||||
for policy_ext in filter(lambda x: isinstance(x.value, x509.CertificatePolicies), cert.extensions):
|
|
||||||
if any(policy.policy_identifier == match_oid for policy in policy_ext.value._policies):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
ID_RSP = "2.23.146.1"
|
|
||||||
ID_RSP_CERT_OBJECTS = '.'.join([ID_RSP, '2'])
|
|
||||||
ID_RSP_ROLE = '.'.join([ID_RSP_CERT_OBJECTS, '1'])
|
|
||||||
|
|
||||||
class oid:
|
|
||||||
id_rspRole_ci = x509.ObjectIdentifier(ID_RSP_ROLE + '.0')
|
|
||||||
id_rspRole_euicc_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.1')
|
|
||||||
id_rspRole_eum_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.2')
|
|
||||||
id_rspRole_dp_tls_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.3')
|
|
||||||
id_rspRole_dp_auth_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.4')
|
|
||||||
id_rspRole_dp_pb_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.5')
|
|
||||||
id_rspRole_ds_tls_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.6')
|
|
||||||
id_rspRole_ds_auth_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.7')
|
|
||||||
|
|
||||||
class VerifyError(Exception):
|
|
||||||
"""An error during certificate verification,"""
|
|
||||||
|
|
||||||
class CertificateSet:
|
|
||||||
"""A set of certificates consisting of a trusted [self-signed] CA root certificate,
|
|
||||||
and an optional number of intermediate certificates. Can be used to verify the certificate chain
|
|
||||||
of any given other certificate."""
|
|
||||||
def __init__(self, root_cert: x509.Certificate):
|
|
||||||
check_signed(root_cert, root_cert)
|
|
||||||
# TODO: check other mandatory attributes for CA Cert
|
|
||||||
if not cert_policy_has_oid(root_cert, oid.id_rspRole_ci):
|
|
||||||
raise ValueError("Given root certificate doesn't have rspRole_ci OID")
|
|
||||||
usage_ext = root_cert.extensions.get_extension_for_class(x509.KeyUsage).value
|
|
||||||
if not usage_ext.key_cert_sign:
|
|
||||||
raise ValueError('Given root certificate key usage does not permit signing of certificates')
|
|
||||||
if not usage_ext.crl_sign:
|
|
||||||
raise ValueError('Given root certificate key usage does not permit signing of CRLs')
|
|
||||||
self.root_cert = root_cert
|
|
||||||
self.intermediate_certs = {}
|
|
||||||
self.crl = None
|
|
||||||
|
|
||||||
def load_crl(self, urls: Optional[List[str]] = None):
|
|
||||||
if urls and isinstance(urls, str):
|
|
||||||
urls = [urls]
|
|
||||||
if not urls:
|
|
||||||
# generate list of CRL URLs from root CA certificate
|
|
||||||
crl_ext = self.root_cert.extensions.get_extension_for_class(x509.CRLDistributionPoints).value
|
|
||||||
name_list = [x.full_name for x in crl_ext]
|
|
||||||
merged_list = []
|
|
||||||
for n in name_list:
|
|
||||||
merged_list += n
|
|
||||||
uri_list = filter(lambda x: isinstance(x, x509.UniformResourceIdentifier), merged_list)
|
|
||||||
urls = [x.value for x in uri_list]
|
|
||||||
|
|
||||||
for url in urls:
|
|
||||||
try:
|
|
||||||
crl_bytes = requests.get(url, timeout=10)
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
continue
|
|
||||||
crl = x509.load_der_x509_crl(crl_bytes)
|
|
||||||
if not crl.is_signature_valid(self.root_cert.public_key()):
|
|
||||||
raise ValueError('Given CRL has incorrect signature and cannot be trusted')
|
|
||||||
# FIXME: various other checks
|
|
||||||
self.crl = crl
|
|
||||||
# FIXME: should we support multiple CRLs? we only support a single CRL right now
|
|
||||||
return
|
|
||||||
# FIXME: report on success/failure
|
|
||||||
|
|
||||||
@property
|
|
||||||
def root_cert_id(self) -> bytes:
|
|
||||||
return cert_get_subject_key_id(self.root_cert)
|
|
||||||
|
|
||||||
def add_intermediate_cert(self, cert: x509.Certificate):
|
|
||||||
"""Add a potential intermediate certificate to the CertificateSet."""
|
|
||||||
# TODO: check mandatory attributes for intermediate cert
|
|
||||||
usage_ext = cert.extensions.get_extension_for_class(x509.KeyUsage).value
|
|
||||||
if not usage_ext.key_cert_sign:
|
|
||||||
raise ValueError('Given intermediate certificate key usage does not permit signing of certificates')
|
|
||||||
aki = cert_get_auth_key_id(cert)
|
|
||||||
ski = cert_get_subject_key_id(cert)
|
|
||||||
if aki == ski:
|
|
||||||
raise ValueError('Cannot add self-signed cert as intermediate cert')
|
|
||||||
self.intermediate_certs[ski] = cert
|
|
||||||
# TODO: we could test if this cert verifies against the root, and mark it as pre-verified
|
|
||||||
# so we don't need to verify again and again the chain of intermediate certificates
|
|
||||||
|
|
||||||
def verify_cert_crl(self, cert: x509.Certificate):
|
|
||||||
if not self.crl:
|
|
||||||
# we cannot check if there's no CRL
|
|
||||||
return
|
|
||||||
if self.crl.get_revoked_certificate_by_serial_number(cert.serial_nr):
|
|
||||||
raise VerifyError('Certificate is present in CRL, verification failed')
|
|
||||||
|
|
||||||
def verify_cert_chain(self, cert: x509.Certificate, max_depth: int = 100):
|
|
||||||
"""Verify if a given certificate's signature chain can be traced back to the root CA of this
|
|
||||||
CertificateSet."""
|
|
||||||
depth = 1
|
|
||||||
c = cert
|
|
||||||
while True:
|
|
||||||
aki = cert_get_auth_key_id(c)
|
|
||||||
if aki == self.root_cert_id:
|
|
||||||
# last step:
|
|
||||||
check_signed(c, self.root_cert)
|
|
||||||
return
|
|
||||||
parent_cert = self.intermediate_certs.get(aki, None)
|
|
||||||
if not parent_cert:
|
|
||||||
raise VerifyError('Could not find intermediate certificate for AuthKeyId %s' % b2h(aki))
|
|
||||||
check_signed(c, parent_cert)
|
|
||||||
# if we reach here, we passed (no exception raised)
|
|
||||||
c = parent_cert
|
|
||||||
depth += 1
|
|
||||||
if depth > max_depth:
|
|
||||||
raise VerifyError('Maximum depth %u exceeded while verifying certificate chain' % max_depth)
|
|
||||||
|
|
||||||
|
|
||||||
def ecdsa_dss_to_tr03111(sig: bytes) -> bytes:
|
|
||||||
"""convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes."""
|
|
||||||
r, s = decode_dss_signature(sig)
|
|
||||||
return r.to_bytes(32, 'big') + s.to_bytes(32, 'big')
|
|
||||||
|
|
||||||
|
|
||||||
class CertAndPrivkey:
|
|
||||||
"""A pair of certificate and private key, as used for ECDSA signing."""
|
|
||||||
def __init__(self, required_policy_oid: Optional[x509.ObjectIdentifier] = None,
|
|
||||||
cert: Optional[x509.Certificate] = None, priv_key = None):
|
|
||||||
self.required_policy_oid = required_policy_oid
|
|
||||||
self.cert = cert
|
|
||||||
self.priv_key = priv_key
|
|
||||||
|
|
||||||
def cert_from_der_file(self, path: str):
|
|
||||||
with open(path, 'rb') as f:
|
|
||||||
cert = x509.load_der_x509_certificate(f.read())
|
|
||||||
if self.required_policy_oid:
|
|
||||||
# verify it is the right type of certificate (id-rspRole-dp-auth, id-rspRole-dp-auth-v2, etc.)
|
|
||||||
assert cert_policy_has_oid(cert, self.required_policy_oid)
|
|
||||||
self.cert = cert
|
|
||||||
|
|
||||||
def privkey_from_pem_file(self, path: str, password: Optional[str] = None):
|
|
||||||
with open(path, 'rb') as f:
|
|
||||||
self.priv_key = load_pem_private_key(f.read(), password)
|
|
||||||
|
|
||||||
def ecdsa_sign(self, plaintext: bytes) -> bytes:
|
|
||||||
"""Sign some input-data using an ECDSA signature compliant with SGP.22,
|
|
||||||
which internally refers to Global Platform 2.2 Annex E, which in turn points
|
|
||||||
to BSI TS-03111 which states "concatenated raw R + S values". """
|
|
||||||
sig = self.priv_key.sign(plaintext, ec.ECDSA(hashes.SHA256()))
|
|
||||||
# convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes
|
|
||||||
return ecdsa_dss_to_tr03111(sig)
|
|
||||||
|
|
||||||
def get_authority_key_identifier(self) -> x509.AuthorityKeyIdentifier:
|
|
||||||
"""Return the AuthorityKeyIdentifier X.509 extension of the certificate."""
|
|
||||||
return list(filter(lambda x: isinstance(x.value, x509.AuthorityKeyIdentifier), self.cert.extensions))[0].value
|
|
||||||
|
|
||||||
def get_subject_alt_name(self) -> x509.SubjectAlternativeName:
|
|
||||||
"""Return the SubjectAlternativeName X.509 extension of the certificate."""
|
|
||||||
return list(filter(lambda x: isinstance(x.value, x509.SubjectAlternativeName), self.cert.extensions))[0].value
|
|
||||||
|
|
||||||
def get_cert_as_der(self) -> bytes:
|
|
||||||
"""Return certificate encoded as DER."""
|
|
||||||
return self.cert.public_bytes(Encoding.DER)
|
|
||||||
|
|
||||||
def get_curve(self) -> ec.EllipticCurve:
|
|
||||||
return self.cert.public_key().public_numbers().curve
|
|
||||||
282
pySim/euicc.py
282
pySim/euicc.py
@@ -1,11 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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.22, GSMA SGP.02, etc.
|
||||||
|
|
||||||
Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
|
# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
|
||||||
@@ -23,57 +21,17 @@ Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from pySim.tlv import *
|
||||||
|
from pySim.construct import *
|
||||||
|
from construct import Optional as COptional
|
||||||
|
from construct import *
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from construct import Array, Struct, FlagsEnum, GreedyRange
|
|
||||||
from cmd2 import cmd2, CommandSet, with_default_category
|
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.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
|
import pySim.global_platform
|
||||||
|
|
||||||
# SGP.02 Section 2.2.2
|
|
||||||
class Sgp02Eid(BER_TLV_IE, tag=0x5a):
|
|
||||||
_construct = BcdAdapter(GreedyBytes)
|
|
||||||
|
|
||||||
# patch this into global_platform, to allow 'get_data sgp02_eid' in EF.ECASD
|
|
||||||
pySim.global_platform.DataCollection.possible_nested.append(Sgp02Eid)
|
|
||||||
|
|
||||||
def compute_eid_checksum(eid) -> str:
|
|
||||||
"""Compute and add/replace check digits of an EID value according to GSMA SGP.29 Section 10."""
|
|
||||||
if isinstance(eid, str):
|
|
||||||
if len(eid) == 30:
|
|
||||||
# first pad by 2 digits
|
|
||||||
eid += "00"
|
|
||||||
elif len(eid) == 32:
|
|
||||||
# zero the last two digits
|
|
||||||
eid = eid[:-2] + "00"
|
|
||||||
else:
|
|
||||||
raise ValueError("and EID must be 30 or 32 digits")
|
|
||||||
eid_int = int(eid)
|
|
||||||
elif isinstance(eid, int):
|
|
||||||
eid_int = eid
|
|
||||||
if eid_int % 100:
|
|
||||||
# zero the last two digits
|
|
||||||
eid_int -= eid_int % 100
|
|
||||||
# Using the resulting 32 digits as a decimal integer, compute the remainder of that number on division by
|
|
||||||
# 97, Subtract the remainder from 98, and use the decimal result for the two check digits, if the result
|
|
||||||
# is one digit long, its value SHALL be prefixed by one digit of 0.
|
|
||||||
csum = 98 - (eid_int % 97)
|
|
||||||
eid_int += csum
|
|
||||||
return str(eid_int)
|
|
||||||
|
|
||||||
def verify_eid_checksum(eid) -> bool:
|
|
||||||
"""Verify the check digits of an EID value according to GSMA SGP.29 Section 10."""
|
|
||||||
# Using the 32 digits as a decimal integer, compute the remainder of that number on division by 97. If the
|
|
||||||
# remainder of the division is 1, the verification is successful; otherwise the EID is invalid.
|
|
||||||
return int(eid) % 97 == 1
|
|
||||||
|
|
||||||
class VersionAdapter(Adapter):
|
class VersionAdapter(Adapter):
|
||||||
"""convert an EUICC Version (3-int array) to a textual representation."""
|
"""convert an EUICC Version (3-int array) to a textual representation."""
|
||||||
|
|
||||||
@@ -91,6 +49,15 @@ AID_ECASD = "A0000005591010FFFFFFFF8900000200"
|
|||||||
AID_ISD_P_FILE = "A0000005591010FFFFFFFF8900000D00"
|
AID_ISD_P_FILE = "A0000005591010FFFFFFFF8900000D00"
|
||||||
AID_ISD_P_MODULE = "A0000005591010FFFFFFFF8900000E00"
|
AID_ISD_P_MODULE = "A0000005591010FFFFFFFF8900000E00"
|
||||||
|
|
||||||
|
sw_isdr = {
|
||||||
|
'ISD-R': {
|
||||||
|
'6a80': 'Incorrect values in command data',
|
||||||
|
'6a82': 'Profile not found',
|
||||||
|
'6a88': 'Reference data not found',
|
||||||
|
'6985': 'Conditions of use not satisfied',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SupportedVersionNumber(BER_TLV_IE, tag=0x82):
|
class SupportedVersionNumber(BER_TLV_IE, tag=0x82):
|
||||||
_construct = GreedyBytes
|
_construct = GreedyBytes
|
||||||
|
|
||||||
@@ -120,7 +87,7 @@ class SetDefaultDpAddress(BER_TLV_IE, tag=0xbf3f, nested=[DefaultDpAddress, SetD
|
|||||||
|
|
||||||
# SGP.22 Section 5.7.7: GetEUICCChallenge
|
# SGP.22 Section 5.7.7: GetEUICCChallenge
|
||||||
class EuiccChallenge(BER_TLV_IE, tag=0x80):
|
class EuiccChallenge(BER_TLV_IE, tag=0x80):
|
||||||
_construct = Bytes(16)
|
_construct = HexAdapter(Bytes(16))
|
||||||
class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
|
class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -128,7 +95,7 @@ class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
|
|||||||
class SVN(BER_TLV_IE, tag=0x82):
|
class SVN(BER_TLV_IE, tag=0x82):
|
||||||
_construct = VersionType
|
_construct = VersionType
|
||||||
class SubjectKeyIdentifier(BER_TLV_IE, tag=0x04):
|
class SubjectKeyIdentifier(BER_TLV_IE, tag=0x04):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
class EuiccCiPkiListForVerification(BER_TLV_IE, tag=0xa9, nested=[SubjectKeyIdentifier]):
|
class EuiccCiPkiListForVerification(BER_TLV_IE, tag=0xa9, nested=[SubjectKeyIdentifier]):
|
||||||
pass
|
pass
|
||||||
class EuiccCiPkiListForSigning(BER_TLV_IE, tag=0xaa, nested=[SubjectKeyIdentifier]):
|
class EuiccCiPkiListForSigning(BER_TLV_IE, tag=0xaa, nested=[SubjectKeyIdentifier]):
|
||||||
@@ -140,15 +107,15 @@ class ProfileVersion(BER_TLV_IE, tag=0x81):
|
|||||||
class EuiccFirmwareVer(BER_TLV_IE, tag=0x83):
|
class EuiccFirmwareVer(BER_TLV_IE, tag=0x83):
|
||||||
_construct = VersionType
|
_construct = VersionType
|
||||||
class ExtCardResource(BER_TLV_IE, tag=0x84):
|
class ExtCardResource(BER_TLV_IE, tag=0x84):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
class UiccCapability(BER_TLV_IE, tag=0x85):
|
class UiccCapability(BER_TLV_IE, tag=0x85):
|
||||||
_construct = GreedyBytes # FIXME
|
_construct = HexAdapter(GreedyBytes) # FIXME
|
||||||
class TS102241Version(BER_TLV_IE, tag=0x86):
|
class TS102241Version(BER_TLV_IE, tag=0x86):
|
||||||
_construct = VersionType
|
_construct = VersionType
|
||||||
class GlobalPlatformVersion(BER_TLV_IE, tag=0x87):
|
class GlobalPlatformVersion(BER_TLV_IE, tag=0x87):
|
||||||
_construct = VersionType
|
_construct = VersionType
|
||||||
class RspCapability(BER_TLV_IE, tag=0x88):
|
class RspCapability(BER_TLV_IE, tag=0x88):
|
||||||
_construct = GreedyBytes # FIXME
|
_construct = HexAdapter(GreedyBytes) # FIXME
|
||||||
class EuiccCategory(BER_TLV_IE, tag=0x8b):
|
class EuiccCategory(BER_TLV_IE, tag=0x8b):
|
||||||
_construct = Enum(Int8ub, other=0, basicEuicc=1, mediumEuicc=2, contactlessEuicc=3)
|
_construct = Enum(Int8ub, other=0, basicEuicc=1, mediumEuicc=2, contactlessEuicc=3)
|
||||||
class PpVersion(BER_TLV_IE, tag=0x04):
|
class PpVersion(BER_TLV_IE, tag=0x04):
|
||||||
@@ -177,7 +144,7 @@ class ProfileMgmtOperation(BER_TLV_IE, tag=0x81):
|
|||||||
class ListNotificationReq(BER_TLV_IE, tag=0xbf28, nested=[ProfileMgmtOperation]):
|
class ListNotificationReq(BER_TLV_IE, tag=0xbf28, nested=[ProfileMgmtOperation]):
|
||||||
pass
|
pass
|
||||||
class SeqNumber(BER_TLV_IE, tag=0x80):
|
class SeqNumber(BER_TLV_IE, tag=0x80):
|
||||||
_construct = Asn1DerInteger()
|
_construct = GreedyInteger()
|
||||||
class NotificationAddress(BER_TLV_IE, tag=0x0c):
|
class NotificationAddress(BER_TLV_IE, tag=0x0c):
|
||||||
_construct = Utf8Adapter(GreedyBytes)
|
_construct = Utf8Adapter(GreedyBytes)
|
||||||
class Iccid(BER_TLV_IE, tag=0x5a):
|
class Iccid(BER_TLV_IE, tag=0x5a):
|
||||||
@@ -211,7 +178,7 @@ class TagList(BER_TLV_IE, tag=0x5c):
|
|||||||
class ProfileInfoListReq(BER_TLV_IE, tag=0xbf2d, nested=[TagList]): # FIXME: SearchCriteria
|
class ProfileInfoListReq(BER_TLV_IE, tag=0xbf2d, nested=[TagList]): # FIXME: SearchCriteria
|
||||||
pass
|
pass
|
||||||
class IsdpAid(BER_TLV_IE, tag=0x4f):
|
class IsdpAid(BER_TLV_IE, tag=0x4f):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
class ProfileState(BER_TLV_IE, tag=0x9f70):
|
class ProfileState(BER_TLV_IE, tag=0x9f70):
|
||||||
_construct = Enum(Int8ub, disabled=0, enabled=1)
|
_construct = Enum(Int8ub, disabled=0, enabled=1)
|
||||||
class ProfileNickname(BER_TLV_IE, tag=0x90):
|
class ProfileNickname(BER_TLV_IE, tag=0x90):
|
||||||
@@ -268,20 +235,9 @@ class DeleteProfileReq(BER_TLV_IE, tag=0xbf33, nested=[IsdpAid, Iccid]):
|
|||||||
class DeleteProfileResp(BER_TLV_IE, tag=0xbf33, nested=[DeleteResult]):
|
class DeleteProfileResp(BER_TLV_IE, tag=0xbf33, nested=[DeleteResult]):
|
||||||
pass
|
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
|
# SGP.22 Section 5.7.20 GetEID
|
||||||
class EidValue(BER_TLV_IE, tag=0x5a):
|
class EidValue(BER_TLV_IE, tag=0x5a):
|
||||||
_construct = GreedyBytes
|
_construct = HexAdapter(GreedyBytes)
|
||||||
class GetEuiccData(BER_TLV_IE, tag=0xbf3e, nested=[TagList, EidValue]):
|
class GetEuiccData(BER_TLV_IE, tag=0xbf3e, nested=[TagList, EidValue]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -302,7 +258,7 @@ class EumCertificate(BER_TLV_IE, tag=0xa5):
|
|||||||
_construct = GreedyBytes
|
_construct = GreedyBytes
|
||||||
class EuiccCertificate(BER_TLV_IE, tag=0xa6):
|
class EuiccCertificate(BER_TLV_IE, tag=0xa6):
|
||||||
_construct = GreedyBytes
|
_construct = GreedyBytes
|
||||||
class GetCertsError(BER_TLV_IE, tag=0x81):
|
class GetCertsError(BER_TLV_IE, tag=0x80):
|
||||||
_construct = Enum(Int8ub, invalidCiPKId=1, undefinedError=127)
|
_construct = Enum(Int8ub, invalidCiPKId=1, undefinedError=127)
|
||||||
class GetCertsResp(BER_TLV_IE, tag=0xbf56, nested=[EumCertificate, EuiccCertificate, GetCertsError]):
|
class GetCertsResp(BER_TLV_IE, tag=0xbf56, nested=[EumCertificate, EuiccCertificate, GetCertsError]):
|
||||||
pass
|
pass
|
||||||
@@ -315,9 +271,9 @@ class EimFqdn(BER_TLV_IE, tag=0x81):
|
|||||||
class EimIdType(BER_TLV_IE, tag=0x82):
|
class EimIdType(BER_TLV_IE, tag=0x82):
|
||||||
_construct = Enum(Int8ub, eimIdTypeOid=1, eimIdTypeFqdn=2, eimIdTypeProprietary=3)
|
_construct = Enum(Int8ub, eimIdTypeOid=1, eimIdTypeFqdn=2, eimIdTypeProprietary=3)
|
||||||
class CounterValue(BER_TLV_IE, tag=0x83):
|
class CounterValue(BER_TLV_IE, tag=0x83):
|
||||||
_construct = Asn1DerInteger()
|
_construct = GreedyInteger
|
||||||
class AssociationToken(BER_TLV_IE, tag=0x84):
|
class AssociationToken(BER_TLV_IE, tag=0x84):
|
||||||
_construct = Asn1DerInteger()
|
_construct = GreedyInteger
|
||||||
class EimSupportedProtocol(BER_TLV_IE, tag=0x87):
|
class EimSupportedProtocol(BER_TLV_IE, tag=0x87):
|
||||||
_construct = Enum(Int8ub, eimRetrieveHttps=0, eimRetrieveCoaps=1, eimInjectHttps=2, eimInjectCoaps=3,
|
_construct = Enum(Int8ub, eimRetrieveHttps=0, eimRetrieveCoaps=1, eimInjectHttps=2, eimInjectCoaps=3,
|
||||||
eimProprietary=4)
|
eimProprietary=4)
|
||||||
@@ -330,24 +286,21 @@ class EimConfigurationDataSeq(BER_TLV_IE, tag=0xa0, nested=[EimConfigurationData
|
|||||||
class GetEimConfigurationData(BER_TLV_IE, tag=0xbf55, nested=[EimConfigurationDataSeq]):
|
class GetEimConfigurationData(BER_TLV_IE, tag=0xbf55, nested=[EimConfigurationDataSeq]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
class ADF_ISDR(CardADF):
|
||||||
def __init__(self):
|
def __init__(self, aid=AID_ISD_R, name='ADF.ISD-R', fid=None, sfid=None,
|
||||||
super().__init__(name='ADF.ISD-R', aid=AID_ISD_R,
|
desc='ISD-R (Issuer Security Domain Root) Application'):
|
||||||
desc='ISD-R (Issuer Security Domain Root) Application')
|
super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
|
||||||
self.adf.decode_select_response = self.decode_select_response
|
self.shell_commands += [self.AddlShellCommands()]
|
||||||
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
|
@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.
|
"""Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22.
|
||||||
Only single-block store supported for now."""
|
Only single-block store supported for now."""
|
||||||
capdu = '80E29100%02x%s00' % (len(tx_do)//2, tx_do)
|
capdu = '%sE29100%02x%s' % (scc.cla4lchan('80'), len(tx_do)//2, tx_do)
|
||||||
return scc.send_apdu_checksw(capdu, exp_sw)
|
return scc._tp.send_apdu_checksw(capdu)
|
||||||
|
|
||||||
@staticmethod
|
@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
|
"""Transceive STORE DATA APDU with the card, transparently encoding the command data from TLV
|
||||||
and decoding the response data tlv."""
|
and decoding the response data tlv."""
|
||||||
if cmd_do:
|
if cmd_do:
|
||||||
@@ -357,7 +310,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
return ValueError('DO > 255 bytes not supported yet')
|
return ValueError('DO > 255 bytes not supported yet')
|
||||||
else:
|
else:
|
||||||
cmd_do_enc = b''
|
cmd_do_enc = b''
|
||||||
(data, _sw) = CardApplicationISDR.store_data(scc, b2h(cmd_do_enc), exp_sw=exp_sw)
|
(data, sw) = ADF_ISDR.store_data(scc, b2h(cmd_do_enc))
|
||||||
if data:
|
if data:
|
||||||
if resp_cls:
|
if resp_cls:
|
||||||
resp_do = resp_cls()
|
resp_do = resp_cls()
|
||||||
@@ -368,13 +321,6 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
else:
|
else:
|
||||||
return None
|
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:
|
def decode_select_response(self, data_hex: Hexstr) -> object:
|
||||||
t = FciTemplate()
|
t = FciTemplate()
|
||||||
t.from_tlv(h2b(data_hex))
|
t.from_tlv(h2b(data_hex))
|
||||||
@@ -390,11 +336,11 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
@cmd2.with_argparser(es10x_store_data_parser)
|
@cmd2.with_argparser(es10x_store_data_parser)
|
||||||
def do_es10x_store_data(self, opts):
|
def do_es10x_store_data(self, opts):
|
||||||
"""Perform a raw STORE DATA command as defined for the ES10x eUICC interface."""
|
"""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) = ADF_ISDR.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."""
|
"""Perform an ES10a GetEuiccConfiguredAddresses function."""
|
||||||
eca = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccConfiguredAddresses(), EuiccConfiguredAddresses)
|
eca = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccConfiguredAddresses(), EuiccConfiguredAddresses)
|
||||||
d = eca.to_dict()
|
d = eca.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['euicc_configured_addresses']))
|
self._cmd.poutput_json(flatten_dict_lists(d['euicc_configured_addresses']))
|
||||||
|
|
||||||
@@ -405,31 +351,31 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
def do_set_default_dp_address(self, opts):
|
def do_set_default_dp_address(self, opts):
|
||||||
"""Perform an ES10a SetDefaultDpAddress function."""
|
"""Perform an ES10a SetDefaultDpAddress function."""
|
||||||
sdda_cmd = SetDefaultDpAddress(children=[DefaultDpAddress(decoded=opts.DP_ADDRESS)])
|
sdda_cmd = SetDefaultDpAddress(children=[DefaultDpAddress(decoded=opts.DP_ADDRESS)])
|
||||||
sdda = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sdda_cmd, SetDefaultDpAddress)
|
sdda = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, sdda_cmd, SetDefaultDpAddress)
|
||||||
d = sdda.to_dict()
|
d = sdda.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['set_default_dp_address']))
|
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."""
|
"""Perform an ES10b GetEUICCChallenge function."""
|
||||||
gec = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEuiccChallenge(), GetEuiccChallenge)
|
gec = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetEuiccChallenge(), GetEuiccChallenge)
|
||||||
d = gec.to_dict()
|
d = gec.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_challenge']))
|
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."""
|
"""Perform an ES10b GetEUICCInfo (1) function."""
|
||||||
ei1 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo1(), EuiccInfo1)
|
ei1 = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo1(), EuiccInfo1)
|
||||||
d = ei1.to_dict()
|
d = ei1.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['euicc_info1']))
|
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."""
|
"""Perform an ES10b GetEUICCInfo (2) function."""
|
||||||
ei2 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo2(), EuiccInfo2)
|
ei2 = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo2(), EuiccInfo2)
|
||||||
d = ei2.to_dict()
|
d = ei2.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['euicc_info2']))
|
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."""
|
"""Perform an ES10b ListNotification function."""
|
||||||
ln = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
|
ln = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
|
||||||
d = ln.to_dict()
|
d = ln.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['list_notification_resp']))
|
self._cmd.poutput_json(flatten_dict_lists(d['list_notification_resp']))
|
||||||
|
|
||||||
@@ -440,13 +386,13 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
def do_remove_notification_from_list(self, opts):
|
def do_remove_notification_from_list(self, opts):
|
||||||
"""Perform an ES10b RemoveNotificationFromList function."""
|
"""Perform an ES10b RemoveNotificationFromList function."""
|
||||||
rn_cmd = NotificationSentReq(children=[SeqNumber(decoded=opts.SEQ_NR)])
|
rn_cmd = NotificationSentReq(children=[SeqNumber(decoded=opts.SEQ_NR)])
|
||||||
rn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, rn_cmd, NotificationSentResp)
|
rn = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, rn_cmd, NotificationSentResp)
|
||||||
d = rn.to_dict()
|
d = rn.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['notification_sent_resp']))
|
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."""
|
"""Perform an ES10c GetProfilesInfo function."""
|
||||||
pi = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ProfileInfoListReq(), ProfileInfoListResp)
|
pi = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ProfileInfoListReq(), ProfileInfoListResp)
|
||||||
d = pi.to_dict()
|
d = pi.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['profile_info_list_resp']))
|
self._cmd.poutput_json(flatten_dict_lists(d['profile_info_list_resp']))
|
||||||
|
|
||||||
@@ -461,14 +407,11 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
"""Perform an ES10c EnableProfile function."""
|
"""Perform an ES10c EnableProfile function."""
|
||||||
if opts.isdp_aid:
|
if opts.isdp_aid:
|
||||||
p_id = ProfileIdentifier(children=[IsdpAid(decoded=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)])
|
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_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
|
||||||
ep_cmd = EnableProfileReq(children=ep_cmd_contents)
|
ep_cmd = EnableProfileReq(children=ep_cmd_contents)
|
||||||
ep = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ep_cmd, EnableProfileResp)
|
ep = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ep_cmd, EnableProfileResp)
|
||||||
d = ep.to_dict()
|
d = ep.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['enable_profile_resp']))
|
self._cmd.poutput_json(flatten_dict_lists(d['enable_profile_resp']))
|
||||||
|
|
||||||
@@ -483,14 +426,11 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
"""Perform an ES10c DisableProfile function."""
|
"""Perform an ES10c DisableProfile function."""
|
||||||
if opts.isdp_aid:
|
if opts.isdp_aid:
|
||||||
p_id = ProfileIdentifier(children=[IsdpAid(decoded=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)])
|
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_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
|
||||||
dp_cmd = DisableProfileReq(children=dp_cmd_contents)
|
dp_cmd = DisableProfileReq(children=dp_cmd_contents)
|
||||||
dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DisableProfileResp)
|
dp = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DisableProfileResp)
|
||||||
d = dp.to_dict()
|
d = dp.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['disable_profile_resp']))
|
self._cmd.poutput_json(flatten_dict_lists(d['disable_profile_resp']))
|
||||||
|
|
||||||
@@ -504,52 +444,26 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
"""Perform an ES10c DeleteProfile function."""
|
"""Perform an ES10c DeleteProfile function."""
|
||||||
if opts.isdp_aid:
|
if opts.isdp_aid:
|
||||||
p_id = IsdpAid(decoded=opts.isdp_aid)
|
p_id = IsdpAid(decoded=opts.isdp_aid)
|
||||||
elif opts.iccid:
|
if opts.iccid:
|
||||||
p_id = Iccid(decoded=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_contents = [p_id]
|
||||||
dp_cmd = DeleteProfileReq(children=dp_cmd_contents)
|
dp_cmd = DeleteProfileReq(children=dp_cmd_contents)
|
||||||
dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DeleteProfileResp)
|
dp = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DeleteProfileResp)
|
||||||
d = dp.to_dict()
|
d = dp.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['delete_profile_resp']))
|
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_get_eid(self, opts):
|
||||||
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):
|
|
||||||
"""Perform an ES10c GetEID function."""
|
"""Perform an ES10c GetEID function."""
|
||||||
|
(data, sw) = ADF_ISDR.store_data(self._cmd.lchan.scc, 'BF3E035C015A')
|
||||||
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
|
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
|
||||||
ged = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
|
ged = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
|
||||||
d = ged.to_dict()
|
d = ged.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_data']))
|
self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_data']))
|
||||||
|
|
||||||
set_nickname_parser = argparse.ArgumentParser()
|
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('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)
|
@cmd2.with_argparser(set_nickname_parser)
|
||||||
def do_set_nickname(self, opts):
|
def do_set_nickname(self, opts):
|
||||||
@@ -557,78 +471,46 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
|||||||
nickname = opts.profile_nickname or ''
|
nickname = opts.profile_nickname or ''
|
||||||
sn_cmd_contents = [Iccid(decoded=opts.ICCID), ProfileNickname(decoded=nickname)]
|
sn_cmd_contents = [Iccid(decoded=opts.ICCID), ProfileNickname(decoded=nickname)]
|
||||||
sn_cmd = SetNicknameReq(children=sn_cmd_contents)
|
sn_cmd = SetNicknameReq(children=sn_cmd_contents)
|
||||||
sn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sn_cmd, SetNicknameResp)
|
sn = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, sn_cmd, SetNicknameResp)
|
||||||
d = sn.to_dict()
|
d = sn.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['set_nickname_resp']))
|
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."""
|
"""Perform an ES10c GetCerts() function on an IoT eUICC."""
|
||||||
gc = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
|
gc = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
|
||||||
d = gc.to_dict()
|
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."""
|
"""Perform an ES10b GetEimConfigurationData function on an Iot eUICC."""
|
||||||
gec = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
|
gec = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
|
||||||
GetEimConfigurationData)
|
GetEimConfigurationData)
|
||||||
d = gec.to_dict()
|
d = gec.to_dict()
|
||||||
self._cmd.poutput_json(flatten_dict_lists(d['get_eim_configuration_data']))
|
self._cmd.poutput_json(flatten_dict_lists(d['get_eim_configuration_data']))
|
||||||
|
|
||||||
class CardApplicationECASD(pySim.global_platform.CardApplicationSD):
|
|
||||||
|
class ADF_ECASD(CardADF):
|
||||||
|
def __init__(self, aid=AID_ECASD, name='ADF.ECASD', fid=None, sfid=None,
|
||||||
|
desc='ECASD (eUICC Controlling Authority Security Domain) Application'):
|
||||||
|
super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
|
||||||
|
self.shell_commands += [self.AddlShellCommands()]
|
||||||
|
|
||||||
def decode_select_response(self, data_hex: Hexstr) -> object:
|
def decode_select_response(self, data_hex: Hexstr) -> object:
|
||||||
t = FciTemplate()
|
t = FciTemplate()
|
||||||
t.from_tlv(h2b(data_hex))
|
t.from_tlv(h2b(data_hex))
|
||||||
d = t.to_dict()
|
d = t.to_dict()
|
||||||
return flatten_dict_lists(d['fci_template'])
|
return flatten_dict_lists(d['fci_template'])
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(name='ADF.ECASD', aid=AID_ECASD,
|
|
||||||
desc='ECASD (eUICC Controlling Authority Security Domain) Application')
|
|
||||||
self.adf.decode_select_response = self.decode_select_response
|
|
||||||
self.adf.shell_commands += [self.AddlShellCommands()]
|
|
||||||
# we attempt to retrieve ECASD key material from CardKeyProvider identified by EID
|
|
||||||
self.adf.scp_key_identity = 'EID'
|
|
||||||
|
|
||||||
@with_default_category('Application-Specific Commands')
|
@with_default_category('Application-Specific Commands')
|
||||||
class AddlShellCommands(CommandSet):
|
class AddlShellCommands(CommandSet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class CardProfileEuiccSGP32(CardProfileUICC):
|
|
||||||
ORDER = 5
|
|
||||||
|
|
||||||
|
|
||||||
|
class CardApplicationISDR(CardApplication):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(name='IoT eUICC (SGP.32)')
|
super().__init__('ISD-R', adf=ADF_ISDR(), sw=sw_isdr)
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
|
class CardApplicationECASD(CardApplication):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(name='Consumer eUICC (SGP.22)')
|
super().__init__('ECASD', adf=ADF_ECASD(), sw=sw_isdr)
|
||||||
|
|
||||||
@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?
|
|
||||||
|
|||||||
@@ -24,14 +24,17 @@
|
|||||||
|
|
||||||
class NoCardError(Exception):
|
class NoCardError(Exception):
|
||||||
"""No card was found in the reader."""
|
"""No card was found in the reader."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProtocolError(Exception):
|
class ProtocolError(Exception):
|
||||||
"""Some kind of protocol level error interfacing with the card."""
|
"""Some kind of protocol level error interfacing with the card."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ReaderError(Exception):
|
class ReaderError(Exception):
|
||||||
"""Some kind of general error with the card reader."""
|
"""Some kind of general error with the card reader."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SwMatchError(Exception):
|
class SwMatchError(Exception):
|
||||||
@@ -49,18 +52,9 @@ class SwMatchError(Exception):
|
|||||||
self.sw_expected = sw_expected
|
self.sw_expected = sw_expected
|
||||||
self.rs = rs
|
self.rs = rs
|
||||||
|
|
||||||
@property
|
def __str__(self):
|
||||||
def description(self):
|
|
||||||
if self.rs and self.rs.lchan[0]:
|
if self.rs and self.rs.lchan[0]:
|
||||||
r = self.rs.lchan[0].interpret_sw(self.sw_actual)
|
r = self.rs.lchan[0].interpret_sw(self.sw_actual)
|
||||||
if r:
|
if r:
|
||||||
return "%s - %s" % (r[0], r[1])
|
return "SW match failed! Expected %s and got %s: %s - %s" % (self.sw_expected, self.sw_actual, r[0], r[1])
|
||||||
return ''
|
return "SW match failed! Expected %s and got %s." % (self.sw_expected, self.sw_actual)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
345
pySim/global_platform.py
Normal file
345
pySim/global_platform.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
"""Partial Support for GlobalPLatform Card Spec (currently 2.1.1)
|
||||||
|
|
||||||
|
(C) 2022-2023 by Harald Welte <laforge@osmocom.org>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, List, Dict, Tuple
|
||||||
|
from construct import Optional as COptional
|
||||||
|
from construct import *
|
||||||
|
from bidict import bidict
|
||||||
|
from pySim.construct import *
|
||||||
|
from pySim.utils import *
|
||||||
|
from pySim.filesystem import *
|
||||||
|
from pySim.tlv import *
|
||||||
|
from pySim.profile import CardProfile
|
||||||
|
|
||||||
|
sw_table = {
|
||||||
|
'Warnings': {
|
||||||
|
'6200': 'Logical Channel already closed',
|
||||||
|
'6283': 'Card Life Cycle State is CARD_LOCKED',
|
||||||
|
'6310': 'More data available',
|
||||||
|
},
|
||||||
|
'Execution errors': {
|
||||||
|
'6400': 'No specific diagnosis',
|
||||||
|
'6581': 'Memory failure',
|
||||||
|
},
|
||||||
|
'Checking errors': {
|
||||||
|
'6700': 'Wrong length in Lc',
|
||||||
|
},
|
||||||
|
'Functions in CLA not supported': {
|
||||||
|
'6881': 'Logical channel not supported or active',
|
||||||
|
'6882': 'Secure messaging not supported',
|
||||||
|
},
|
||||||
|
'Command not allowed': {
|
||||||
|
'6982': 'Security Status not satisfied',
|
||||||
|
'6985': 'Conditions of use not satisfied',
|
||||||
|
},
|
||||||
|
'Wrong parameters': {
|
||||||
|
'6a80': 'Incorrect values in command data',
|
||||||
|
'6a81': 'Function not supported e.g. card Life Cycle State is CARD_LOCKED',
|
||||||
|
'6a82': 'Application not found',
|
||||||
|
'6a84': 'Not enough memory space',
|
||||||
|
'6a86': 'Incorrect P1 P2',
|
||||||
|
'6a88': 'Referenced data not found',
|
||||||
|
},
|
||||||
|
'GlobalPlatform': {
|
||||||
|
'6d00': 'Invalid instruction',
|
||||||
|
'6e00': 'Invalid class',
|
||||||
|
},
|
||||||
|
'Application errors': {
|
||||||
|
'9484': 'Algorithm not supported',
|
||||||
|
'9485': 'Invalid key check value',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.1.6
|
||||||
|
KeyType = Enum(Byte, des=0x80,
|
||||||
|
tls_psk=0x85, # v2.3.1 Section 11.1.8
|
||||||
|
aes=0x88, # v2.3.1 Section 11.1.8
|
||||||
|
hmac_sha1=0x90, # v2.3.1 Section 11.1.8
|
||||||
|
hmac_sha1_160=0x91, # v2.3.1 Section 11.1.8
|
||||||
|
rsa_public_exponent_e_cleartex=0xA0,
|
||||||
|
rsa_modulus_n_cleartext=0xA1,
|
||||||
|
rsa_modulus_n=0xA2,
|
||||||
|
rsa_private_exponent_d=0xA3,
|
||||||
|
rsa_chines_remainder_p=0xA4,
|
||||||
|
rsa_chines_remainder_q=0xA5,
|
||||||
|
rsa_chines_remainder_pq=0xA6,
|
||||||
|
rsa_chines_remainder_dpi=0xA7,
|
||||||
|
rsa_chines_remainder_dqi=0xA8,
|
||||||
|
ecc_public_key=0xB0, # v2.3.1 Section 11.1.8
|
||||||
|
ecc_private_key=0xB1, # v2.3.1 Section 11.1.8
|
||||||
|
ecc_field_parameter_p=0xB2, # v2.3.1 Section 11.1.8
|
||||||
|
ecc_field_parameter_a=0xB3, # v2.3.1 Section 11.1.8
|
||||||
|
ecc_field_parameter_b=0xB4, # v2.3.1 Section 11.1.8
|
||||||
|
ecc_field_parameter_g=0xB5, # v2.3.1 Section 11.1.8
|
||||||
|
ecc_field_parameter_n=0xB6, # v2.3.1 Section 11.1.8
|
||||||
|
ecc_field_parameter_k=0xB7, # v2.3.1 Section 11.1.8
|
||||||
|
ecc_key_parameters_reference=0xF0, # v2.3.1 Section 11.1.8
|
||||||
|
not_available=0xff)
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.3.3.1
|
||||||
|
class KeyInformationData(BER_TLV_IE, tag=0xc0):
|
||||||
|
_test_de_encode = [
|
||||||
|
( 'c00401708010', {"key_identifier": 1, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00402708010', {"key_identifier": 2, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00403708010', {"key_identifier": 3, "key_version_number": 112, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00401018010', {"key_identifier": 1, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00402018010', {"key_identifier": 2, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00403018010', {"key_identifier": 3, "key_version_number": 1, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00401028010', {"key_identifier": 1, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00402028010', {"key_identifier": 2, "key_version_number": 2, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00403038010', {"key_identifier": 3, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00401038010', {"key_identifier": 1, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00402038010', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "des"} ]} ),
|
||||||
|
( 'c00402038810', {"key_identifier": 2, "key_version_number": 3, "key_types": [ {"length": 16, "type": "aes"} ]} ),
|
||||||
|
]
|
||||||
|
KeyTypeLen = Struct('type'/KeyType, 'length'/Int8ub)
|
||||||
|
_construct = Struct('key_identifier'/Byte, 'key_version_number'/Byte,
|
||||||
|
'key_types'/GreedyRange(KeyTypeLen))
|
||||||
|
class KeyInformation(BER_TLV_IE, tag=0xe0, nested=[KeyInformationData]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# GlobalPlatform v2.3.1 Section H.4
|
||||||
|
class ScpInformation(BER_TLV_IE, tag=0xa0):
|
||||||
|
pass
|
||||||
|
class PrivilegesAvailableSSD(BER_TLV_IE, tag=0x81):
|
||||||
|
pass
|
||||||
|
class PrivilegesAvailableApplication(BER_TLV_IE, tag=0x82):
|
||||||
|
pass
|
||||||
|
class SupportedLFDBHAlgorithms(BER_TLV_IE, tag=0x83):
|
||||||
|
pass
|
||||||
|
class CiphersForLFDBEncryption(BER_TLV_IE, tag=0x84):
|
||||||
|
pass
|
||||||
|
class CiphersForTokens(BER_TLV_IE, tag=0x85):
|
||||||
|
pass
|
||||||
|
class CiphersForReceipts(BER_TLV_IE, tag=0x86):
|
||||||
|
pass
|
||||||
|
class CiphersForDAPs(BER_TLV_IE, tag=0x87):
|
||||||
|
pass
|
||||||
|
class KeyParameterReferenceList(BER_TLV_IE, tag=0x88):
|
||||||
|
pass
|
||||||
|
class CardCapabilityInformation(BER_TLV_IE, tag=0x67, nested=[ScpInformation, PrivilegesAvailableSSD,
|
||||||
|
PrivilegesAvailableApplication,
|
||||||
|
SupportedLFDBHAlgorithms,
|
||||||
|
CiphersForLFDBEncryption, CiphersForTokens,
|
||||||
|
CiphersForReceipts, CiphersForDAPs,
|
||||||
|
KeyParameterReferenceList]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
|
||||||
|
_construct = Int8ub
|
||||||
|
|
||||||
|
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
|
||||||
|
class ApplicationAID(BER_TLV_IE, tag=0x4f):
|
||||||
|
_construct = HexAdapter(GreedyBytes)
|
||||||
|
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
|
||||||
|
pass
|
||||||
|
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# GlobalPlatform v2.3.1 Section 11.3.3.1.2 + TS 102 226
|
||||||
|
class NumberOFInstalledApp(BER_TLV_IE, tag=0x81):
|
||||||
|
_construct = GreedyInteger()
|
||||||
|
class FreeNonVolatileMemory(BER_TLV_IE, tag=0x82):
|
||||||
|
_construct = GreedyInteger()
|
||||||
|
class FreeVolatileMemory(BER_TLV_IE, tag=0x83):
|
||||||
|
_construct = GreedyInteger()
|
||||||
|
class ExtendedCardResourcesInfo(BER_TLV_IE, tag=0xff21, nested=[NumberOFInstalledApp, FreeNonVolatileMemory,
|
||||||
|
FreeVolatileMemory]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# GlobalPlatform v2.3.1 Section 7.4.2.4 + GP SPDM
|
||||||
|
class SecurityDomainManagerURL(BER_TLV_IE, tag=0x5f50):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# card data sample, returned in response to GET DATA (80ca006600):
|
||||||
|
# 66 31
|
||||||
|
# 73 2f
|
||||||
|
# 06 07
|
||||||
|
# 2a864886fc6b01
|
||||||
|
# 60 0c
|
||||||
|
# 06 0a
|
||||||
|
# 2a864886fc6b02020101
|
||||||
|
# 63 09
|
||||||
|
# 06 07
|
||||||
|
# 2a864886fc6b03
|
||||||
|
# 64 0b
|
||||||
|
# 06 09
|
||||||
|
# 2a864886fc6b040215
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Table F-1
|
||||||
|
class ObjectIdentifier(BER_TLV_IE, tag=0x06):
|
||||||
|
_construct = GreedyBytes
|
||||||
|
class CardManagementTypeAndVersion(BER_TLV_IE, tag=0x60, nested=[ObjectIdentifier]):
|
||||||
|
pass
|
||||||
|
class CardIdentificationScheme(BER_TLV_IE, tag=0x63, nested=[ObjectIdentifier]):
|
||||||
|
pass
|
||||||
|
class SecureChannelProtocolOfISD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
|
||||||
|
pass
|
||||||
|
class CardConfigurationDetails(BER_TLV_IE, tag=0x65):
|
||||||
|
_construct = GreedyBytes
|
||||||
|
class CardChipDetails(BER_TLV_IE, tag=0x66):
|
||||||
|
_construct = GreedyBytes
|
||||||
|
class CardRecognitionData(BER_TLV_IE, tag=0x73, nested=[ObjectIdentifier,
|
||||||
|
CardManagementTypeAndVersion,
|
||||||
|
CardIdentificationScheme,
|
||||||
|
SecureChannelProtocolOfISD,
|
||||||
|
CardConfigurationDetails,
|
||||||
|
CardChipDetails]):
|
||||||
|
pass
|
||||||
|
class CardData(BER_TLV_IE, tag=0x66, nested=[CardRecognitionData]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Table F-2
|
||||||
|
class SecureChannelProtocolOfSelectedSD(BER_TLV_IE, tag=0x64, nested=[ObjectIdentifier]):
|
||||||
|
pass
|
||||||
|
class SecurityDomainMgmtData(BER_TLV_IE, tag=0x73, nested=[CardManagementTypeAndVersion,
|
||||||
|
CardIdentificationScheme,
|
||||||
|
SecureChannelProtocolOfSelectedSD,
|
||||||
|
CardConfigurationDetails,
|
||||||
|
CardChipDetails]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.1.1
|
||||||
|
IsdLifeCycleState = Enum(Byte, op_ready=0x01, initialized=0x07, secured=0x0f,
|
||||||
|
card_locked = 0x7f, terminated=0xff)
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||||
|
class ApplicationID(BER_TLV_IE, tag=0x84):
|
||||||
|
_construct = GreedyBytes
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||||
|
class SecurityDomainManagementData(BER_TLV_IE, tag=0x73):
|
||||||
|
_construct = GreedyBytes
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||||
|
class ApplicationProductionLifeCycleData(BER_TLV_IE, tag=0x9f6e):
|
||||||
|
_construct = GreedyBytes
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||||
|
class MaximumLengthOfDataFieldInCommandMessage(BER_TLV_IE, tag=0x9f65):
|
||||||
|
_construct = GreedyInteger()
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||||
|
class ProprietaryData(BER_TLV_IE, tag=0xA5, nested=[SecurityDomainManagementData,
|
||||||
|
ApplicationProductionLifeCycleData,
|
||||||
|
MaximumLengthOfDataFieldInCommandMessage]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# explicitly define this list and give it a name so pySim.euicc can reference it
|
||||||
|
FciTemplateNestedList = [ApplicationID, SecurityDomainManagementData,
|
||||||
|
ApplicationProductionLifeCycleData,
|
||||||
|
MaximumLengthOfDataFieldInCommandMessage,
|
||||||
|
ProprietaryData]
|
||||||
|
|
||||||
|
# GlobalPlatform 2.1.1 Section 9.9.3.1
|
||||||
|
class FciTemplate(BER_TLV_IE, tag=0x6f, nested=FciTemplateNestedList):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
|
||||||
|
_construct = BcdAdapter(GreedyBytes)
|
||||||
|
|
||||||
|
class CardImageNumber(BER_TLV_IE, tag=0x45):
|
||||||
|
_construct = BcdAdapter(GreedyBytes)
|
||||||
|
|
||||||
|
class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
|
||||||
|
_construct = GreedyInteger()
|
||||||
|
|
||||||
|
class ConfirmationCounter(BER_TLV_IE, tag=0xc2):
|
||||||
|
_construct = GreedyInteger()
|
||||||
|
|
||||||
|
# Collection of all the data objects we can get from GET DATA
|
||||||
|
class DataCollection(TLV_IE_Collection, nested=[IssuerIdentificationNumber,
|
||||||
|
CardImageNumber,
|
||||||
|
CardData,
|
||||||
|
KeyInformation,
|
||||||
|
SequenceCounterOfDefaultKvn,
|
||||||
|
ConfirmationCounter,
|
||||||
|
# v2.3.1
|
||||||
|
CardCapabilityInformation,
|
||||||
|
CurrentSecurityLevel,
|
||||||
|
ListOfApplications,
|
||||||
|
ExtendedCardResourcesInfo,
|
||||||
|
SecurityDomainManagerURL]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def decode_select_response(resp_hex: str) -> object:
|
||||||
|
t = FciTemplate()
|
||||||
|
t.from_tlv(h2b(resp_hex))
|
||||||
|
d = t.to_dict()
|
||||||
|
return flatten_dict_lists(d['fci_template'])
|
||||||
|
|
||||||
|
# Application Dedicated File of a Security Domain
|
||||||
|
class ADF_SD(CardADF):
|
||||||
|
def __init__(self, aid: str, name: str, desc: str):
|
||||||
|
super().__init__(aid=aid, fid=None, sfid=None, name=name, desc=desc)
|
||||||
|
self.shell_commands += [self.AddlShellCommands()]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode_select_response(res_hex: str) -> object:
|
||||||
|
return decode_select_response(res_hex)
|
||||||
|
|
||||||
|
@with_default_category('Application-Specific Commands')
|
||||||
|
class AddlShellCommands(CommandSet):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
get_data_parser = argparse.ArgumentParser()
|
||||||
|
get_data_parser.add_argument('data_object_name', type=str,
|
||||||
|
help='Name of the data object to be retrieved from the card')
|
||||||
|
|
||||||
|
@cmd2.with_argparser(get_data_parser)
|
||||||
|
def do_get_data(self, opts):
|
||||||
|
"""Perform the GlobalPlatform GET DATA command in order to obtain some card-specific data."""
|
||||||
|
tlv_cls_name = opts.data_object_name
|
||||||
|
try:
|
||||||
|
tlv_cls = DataCollection().members_by_name[tlv_cls_name]
|
||||||
|
except KeyError:
|
||||||
|
do_names = [camel_to_snake(str(x.__name__)) for x in DataCollection.possible_nested]
|
||||||
|
self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name,
|
||||||
|
do_names))
|
||||||
|
return
|
||||||
|
(data, sw) = self._cmd.lchan.scc.get_data(cla=0x80, tag=tlv_cls.tag)
|
||||||
|
ie = tlv_cls()
|
||||||
|
ie.from_tlv(h2b(data))
|
||||||
|
self._cmd.poutput_json(ie.to_dict())
|
||||||
|
|
||||||
|
def complete_get_data(self, text, line, begidx, endidx) -> List[str]:
|
||||||
|
data_dict = {camel_to_snake(str(x.__name__)): x for x in DataCollection.possible_nested}
|
||||||
|
index_dict = {1: data_dict}
|
||||||
|
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
|
||||||
|
|
||||||
|
# Card Application of a Security Domain
|
||||||
|
class CardApplicationSD(CardApplication):
|
||||||
|
__intermediate = True
|
||||||
|
def __init__(self, aid: str, name: str, desc: str):
|
||||||
|
super().__init__(name, adf=ADF_SD(aid, name, desc), sw=sw_table)
|
||||||
|
|
||||||
|
# Card Application of Issuer Security Domain
|
||||||
|
class CardApplicationISD(CardApplicationSD):
|
||||||
|
# FIXME: ISD AID is not static, but could be different. One can select the empty
|
||||||
|
# application using '00a4040000' and then parse the response FCI to get the ISD AID
|
||||||
|
def __init__(self, aid='a000000003000000'):
|
||||||
|
super().__init__(aid=aid, name='ADF.ISD', desc='Issuer Security Domain')
|
||||||
|
|
||||||
|
#class CardProfileGlobalPlatform(CardProfile):
|
||||||
|
# ORDER = 23
|
||||||
|
#
|
||||||
|
# def __init__(self, name='GlobalPlatform'):
|
||||||
|
# super().__init__(name, desc='GlobalPlatfomr 2.1.1', cla=['00','80','84'], sw=sw_table)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
@@ -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())
|
|
||||||
@@ -1,606 +0,0 @@
|
|||||||
# Global Platform SCP02 + SCP03 (Secure Channel Protocol) implementation
|
|
||||||
#
|
|
||||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
from Cryptodome.Cipher import DES3, DES
|
|
||||||
from Cryptodome.Util.strxor import strxor
|
|
||||||
from construct import Struct, 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.secure_channel import SecureChannel
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
def scp02_key_derivation(constant: bytes, counter: int, base_key: bytes) -> bytes:
|
|
||||||
assert len(constant) == 2
|
|
||||||
assert(counter >= 0 and counter <= 65535)
|
|
||||||
assert len(base_key) == 16
|
|
||||||
|
|
||||||
derivation_data = constant + counter.to_bytes(2, 'big') + b'\x00' * 12
|
|
||||||
cipher = DES3.new(base_key, DES.MODE_CBC, b'\x00' * 8)
|
|
||||||
return cipher.encrypt(derivation_data)
|
|
||||||
|
|
||||||
# TODO: resolve duplication with BspAlgoCryptAES128
|
|
||||||
def pad80(s: bytes, BS=8) -> bytes:
|
|
||||||
""" Pad bytestring s: add '\x80' and '\0'* so the result to be multiple of BS."""
|
|
||||||
l = BS-1 - len(s) % BS
|
|
||||||
return s + b'\x80' + b'\0'*l
|
|
||||||
|
|
||||||
# TODO: resolve duplication with BspAlgoCryptAES128
|
|
||||||
def unpad80(padded: bytes) -> bytes:
|
|
||||||
"""Remove the customary 80 00 00 ... padding used for AES."""
|
|
||||||
# first remove any trailing zero bytes
|
|
||||||
stripped = padded.rstrip(b'\0')
|
|
||||||
# then remove the final 80
|
|
||||||
assert stripped[-1] == 0x80
|
|
||||||
return stripped[:-1]
|
|
||||||
|
|
||||||
class Scp02SessionKeys:
|
|
||||||
"""A single set of GlobalPlatform session keys."""
|
|
||||||
DERIV_CONST_CMAC = b'\x01\x01'
|
|
||||||
DERIV_CONST_RMAC = b'\x01\x02'
|
|
||||||
DERIV_CONST_ENC = b'\x01\x82'
|
|
||||||
DERIV_CONST_DENC = b'\x01\x81'
|
|
||||||
blocksize = 8
|
|
||||||
|
|
||||||
def calc_mac_1des(self, data: bytes, reset_icv: bool = False) -> bytes:
|
|
||||||
"""Pad and calculate MAC according to B.1.2.2 - Single DES plus final 3DES"""
|
|
||||||
e = DES.new(self.c_mac[:8], DES.MODE_ECB)
|
|
||||||
d = DES.new(self.c_mac[8:], DES.MODE_ECB)
|
|
||||||
padded_data = pad80(data, 8)
|
|
||||||
q = len(padded_data) // 8
|
|
||||||
icv = b'\x00' * 8 if reset_icv else self.icv
|
|
||||||
h = icv
|
|
||||||
for i in range(q):
|
|
||||||
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
|
|
||||||
h = d.decrypt(h)
|
|
||||||
h = e.encrypt(h)
|
|
||||||
logger.debug("mac_1des(%s,icv=%s) -> %s", b2h(data), b2h(icv), b2h(h))
|
|
||||||
if self.des_icv_enc:
|
|
||||||
self.icv = self.des_icv_enc.encrypt(h)
|
|
||||||
else:
|
|
||||||
self.icv = h
|
|
||||||
return h
|
|
||||||
|
|
||||||
def calc_mac_3des(self, data: bytes) -> bytes:
|
|
||||||
e = DES3.new(self.enc, DES.MODE_ECB)
|
|
||||||
padded_data = pad80(data, 8)
|
|
||||||
q = len(padded_data) // 8
|
|
||||||
h = b'\x00' * 8
|
|
||||||
for i in range(q):
|
|
||||||
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
|
|
||||||
logger.debug("mac_3des(%s) -> %s", b2h(data), b2h(h))
|
|
||||||
return h
|
|
||||||
|
|
||||||
def __init__(self, counter: int, card_keys: 'GpCardKeyset', icv_encrypt=True):
|
|
||||||
self.icv = None
|
|
||||||
self.counter = counter
|
|
||||||
self.card_keys = card_keys
|
|
||||||
self.c_mac = scp02_key_derivation(self.DERIV_CONST_CMAC, self.counter, card_keys.mac)
|
|
||||||
self.r_mac = scp02_key_derivation(self.DERIV_CONST_RMAC, self.counter, card_keys.mac)
|
|
||||||
self.enc = scp02_key_derivation(self.DERIV_CONST_ENC, self.counter, card_keys.enc)
|
|
||||||
self.data_enc = scp02_key_derivation(self.DERIV_CONST_DENC, self.counter, card_keys.dek)
|
|
||||||
self.des_icv_enc = DES.new(self.c_mac[:8], DES.MODE_ECB) if icv_encrypt else None
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "%s(CTR=%u, ICV=%s, ENC=%s, D-ENC=%s, MAC-C=%s, MAC-R=%s)" % (
|
|
||||||
self.__class__.__name__, self.counter, b2h(self.icv) if self.icv else "None",
|
|
||||||
b2h(self.enc), b2h(self.data_enc), b2h(self.c_mac), b2h(self.r_mac))
|
|
||||||
|
|
||||||
INS_INIT_UPDATE = 0x50
|
|
||||||
INS_EXT_AUTH = 0x82
|
|
||||||
CLA_SM = 0x04
|
|
||||||
|
|
||||||
class SCP(SecureChannel, abc.ABC):
|
|
||||||
"""Abstract base class containing some common interface + functionality for SCP protocols."""
|
|
||||||
def __init__(self, card_keys: 'GpCardKeyset', lchan_nr: int = 0):
|
|
||||||
|
|
||||||
# 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 not card_keys.kvn in range(self.kvn_range[0], self.kvn_range[1]+1):
|
|
||||||
raise ValueError('%s cannot be used with KVN outside range 0x%02x..0x%02x' %
|
|
||||||
(self.__class__.__name__, self.kvn_range[0], self.kvn_range[1]))
|
|
||||||
elif hasattr(self, 'kvn_ranges'):
|
|
||||||
# pylint: disable=no-member
|
|
||||||
if all([not card_keys.kvn in range(x[0], x[1]+1) for x in self.kvn_ranges]):
|
|
||||||
raise ValueError('%s cannot be used with KVN outside permitted ranges %s' %
|
|
||||||
(self.__class__.__name__, self.kvn_ranges))
|
|
||||||
|
|
||||||
self.lchan_nr = lchan_nr
|
|
||||||
self.card_keys = card_keys
|
|
||||||
self.sk = None
|
|
||||||
self.mac_on_unmodified = False
|
|
||||||
self.security_level = 0x00
|
|
||||||
|
|
||||||
@property
|
|
||||||
def do_cmac(self) -> bool:
|
|
||||||
"""Should we perform C-MAC?"""
|
|
||||||
return self.security_level & 0x01
|
|
||||||
|
|
||||||
@property
|
|
||||||
def do_rmac(self) -> bool:
|
|
||||||
"""Should we perform R-MAC?"""
|
|
||||||
return self.security_level & 0x10
|
|
||||||
|
|
||||||
@property
|
|
||||||
def do_cenc(self) -> bool:
|
|
||||||
"""Should we perform C-ENC?"""
|
|
||||||
return self.security_level & 0x02
|
|
||||||
|
|
||||||
@property
|
|
||||||
def do_renc(self) -> bool:
|
|
||||||
"""Should we perform R-ENC?"""
|
|
||||||
return self.security_level & 0x20
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "%s[%02x]" % (self.__class__.__name__, self.security_level)
|
|
||||||
|
|
||||||
def _cla(self, sm: bool = False, b8: bool = True) -> int:
|
|
||||||
ret = 0x80 if b8 else 0x00
|
|
||||||
if sm:
|
|
||||||
ret = ret | CLA_SM
|
|
||||||
return ret + self.lchan_nr
|
|
||||||
|
|
||||||
def wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
|
|
||||||
# Generic handling of GlobalPlatform SCP, implements SecureChannel.wrap_cmd_apdu
|
|
||||||
# only protect those APDUs that actually are global platform commands
|
|
||||||
if apdu[0] & 0x80:
|
|
||||||
return self._wrap_cmd_apdu(apdu, *args, **kwargs)
|
|
||||||
return apdu
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
|
|
||||||
"""Method implementation to be provided by derived class."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def gen_init_update_apdu(self, host_challenge: Optional[bytes]) -> bytes:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def parse_init_update_resp(self, resp_bin: bytes):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def encrypt_key(self, key: bytes) -> bytes:
|
|
||||||
"""Encrypt a key with the DEK."""
|
|
||||||
num_pad = len(key) % self.sk.blocksize
|
|
||||||
if num_pad:
|
|
||||||
return bertlv_encode_len(len(key)) + self.dek_encrypt(key + b'\x00'*num_pad)
|
|
||||||
return self.dek_encrypt(key)
|
|
||||||
|
|
||||||
def decrypt_key(self, encrypted_key:bytes) -> bytes:
|
|
||||||
"""Decrypt a key with the DEK."""
|
|
||||||
if len(encrypted_key) % self.sk.blocksize:
|
|
||||||
# If the length of the Key Component Block is not a multiple of the block size of the encryption #
|
|
||||||
# algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that the key
|
|
||||||
# component value was right-padded prior to encryption and that the Key Component Block was
|
|
||||||
# formatted as described in Table 11-70. In this case, the first byte(s) of the Key Component
|
|
||||||
# Block provides the actual length of the key component value, which allows recovering the
|
|
||||||
# clear-text key component value after decryption of the encrypted key component value and removal
|
|
||||||
# of padding bytes.
|
|
||||||
decrypted = self.dek_decrypt(encrypted_key)
|
|
||||||
key_len, remainder = bertlv_parse_len(decrypted)
|
|
||||||
return remainder[:key_len]
|
|
||||||
else:
|
|
||||||
# If the length of the Key Component Block is a multiple of the block size of the encryption
|
|
||||||
# algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that no padding
|
|
||||||
# bytes were added before encrypting the key component value and that the Key Component Block is
|
|
||||||
# only composed of the encrypted key component value (as shown in Table 11-71). In this case, the
|
|
||||||
# clear-text key component value is simply recovered by decrypting the Key Component Block.
|
|
||||||
return self.dek_decrypt(encrypted_key)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def dek_decrypt(self, ciphertext:bytes) -> bytes:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SCP02(SCP):
|
|
||||||
"""An instance of the GlobalPlatform SCP02 secure channel protocol."""
|
|
||||||
|
|
||||||
constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x02'),
|
|
||||||
'seq_counter'/Int16ub, 'card_challenge'/Bytes(6), 'card_cryptogram'/Bytes(8))
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
|
|
||||||
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge))
|
|
||||||
self.host_cryptogram = self.sk.calc_mac_3des(self.sk.counter.to_bytes(2, 'big') + card_challenge + host_challenge)
|
|
||||||
self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2, 'big') + card_challenge)
|
|
||||||
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
|
|
||||||
|
|
||||||
def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes:
|
|
||||||
"""Generate INITIALIZE UPDATE APDU."""
|
|
||||||
self.host_challenge = host_challenge
|
|
||||||
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge + b'\x00'
|
|
||||||
|
|
||||||
def parse_init_update_resp(self, resp_bin: bytes):
|
|
||||||
"""Parse response to INITIALZIE UPDATE."""
|
|
||||||
resp = self.constr_iur.parse(resp_bin)
|
|
||||||
self.card_challenge = resp['card_challenge']
|
|
||||||
self.sk = Scp02SessionKeys(resp['seq_counter'], self.card_keys)
|
|
||||||
logger.debug(self.sk)
|
|
||||||
self._compute_cryptograms(self.card_challenge, self.host_challenge)
|
|
||||||
if self.card_cryptogram != resp['card_cryptogram']:
|
|
||||||
raise ValueError("card cryptogram doesn't match")
|
|
||||||
|
|
||||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
|
||||||
"""Generate EXTERNAL AUTHENTICATE APDU."""
|
|
||||||
if security_level & 0xf0:
|
|
||||||
raise NotImplementedError('R-MAC/R-ENC for SCP02 not implemented yet.')
|
|
||||||
self.security_level = security_level
|
|
||||||
if self.mac_on_unmodified:
|
|
||||||
header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, 8])
|
|
||||||
else:
|
|
||||||
header = bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16])
|
|
||||||
#return self.wrap_cmd_apdu(header + self.host_cryptogram)
|
|
||||||
mac = self.sk.calc_mac_1des(header + self.host_cryptogram, True)
|
|
||||||
return bytes([self._cla(True), INS_EXT_AUTH, self.security_level, 0, 16]) + self.host_cryptogram + mac
|
|
||||||
|
|
||||||
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
|
|
||||||
"""Wrap Command APDU for SCP02: calculate MAC and encrypt."""
|
|
||||||
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
|
||||||
|
|
||||||
if not self.do_cmac:
|
|
||||||
return apdu
|
|
||||||
|
|
||||||
(case, lc, le, data) = parse_command_apdu(apdu)
|
|
||||||
|
|
||||||
# TODO: add support for extended length fields.
|
|
||||||
assert lc <= 256
|
|
||||||
assert le <= 256
|
|
||||||
lc &= 0xFF
|
|
||||||
le &= 0xFF
|
|
||||||
|
|
||||||
# CLA without log. channel can be 80 or 00 only
|
|
||||||
cla = apdu[0]
|
|
||||||
b8 = cla & 0x80
|
|
||||||
if cla & 0x03 or cla & CLA_SM:
|
|
||||||
# nonzero logical channel in APDU, check that are the same
|
|
||||||
assert cla == self._cla(False, b8), "CLA mismatch"
|
|
||||||
|
|
||||||
if self.mac_on_unmodified:
|
|
||||||
mlc = lc
|
|
||||||
clac = cla
|
|
||||||
else:
|
|
||||||
# CMAC on modified APDU
|
|
||||||
mlc = lc + 8
|
|
||||||
clac = cla | CLA_SM
|
|
||||||
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + data)
|
|
||||||
if self.do_cenc:
|
|
||||||
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
|
|
||||||
data = k.encrypt(pad80(data, 8))
|
|
||||||
lc = len(data)
|
|
||||||
|
|
||||||
lc += 8
|
|
||||||
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
|
|
||||||
|
|
||||||
# Since we attach a signature, we will always send some data. This means that if the APDU is of case #4
|
|
||||||
# or case #2, we must attach an additional Le byte to signal that we expect a response. It is technically
|
|
||||||
# legal to use 0x00 (=256) as Le byte, even when the caller has specified a different value in the original
|
|
||||||
# APDU. This is due to the fact that Le always describes the maximum expected length of the response
|
|
||||||
# (see also ISO/IEC 7816-4, section 5.1). In addition to that, it should also important that depending on
|
|
||||||
# the configuration of the SCP, the response may also contain a signature that makes the response larger
|
|
||||||
# than specified in the Le field of the original APDU.
|
|
||||||
if case == 4 or case == 2:
|
|
||||||
apdu += b'\x00'
|
|
||||||
|
|
||||||
return apdu
|
|
||||||
|
|
||||||
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
|
|
||||||
# TODO: Implement R-MAC / R-ENC
|
|
||||||
return rsp_apdu
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from Cryptodome.Cipher import AES
|
|
||||||
from Cryptodome.Hash import CMAC
|
|
||||||
|
|
||||||
def scp03_key_derivation(constant: bytes, context: bytes, base_key: bytes, l: Optional[int] = None) -> bytes:
|
|
||||||
"""SCP03 Key Derivation Function as specified in Annex D 4.1.5."""
|
|
||||||
# Data derivation shall use KDF in counter mode as specified in NIST SP 800-108 ([NIST 800-108]). The PRF
|
|
||||||
# used in the KDF shall be CMAC as specified in [NIST 800-38B], used with full 16-byte output length.
|
|
||||||
def prf(key: bytes, data:bytes):
|
|
||||||
return CMAC.new(key, data, AES).digest()
|
|
||||||
|
|
||||||
if l is None:
|
|
||||||
l = len(base_key) * 8
|
|
||||||
|
|
||||||
logger.debug("scp03_kdf(constant=%s, context=%s, base_key=%s, l=%u)", b2h(constant), b2h(context), b2h(base_key), l)
|
|
||||||
output_len = l // 8
|
|
||||||
# SCP03 Section 4.1.5 defines a different parameter order than NIST SP 800-108, so we cannot use the
|
|
||||||
# existing Cryptodome.Protocol.KDF.SP800_108_Counter function :(
|
|
||||||
# A 12-byte “label” consisting of 11 bytes with value '00' followed by a 1-byte derivation constant
|
|
||||||
assert len(constant) == 1
|
|
||||||
label = b'\x00' *11 + constant
|
|
||||||
i = 1
|
|
||||||
dk = b''
|
|
||||||
while len(dk) < output_len:
|
|
||||||
# 12B label, 1B separation, 2B L, 1B i, Context
|
|
||||||
info = label + b'\x00' + l.to_bytes(2, 'big') + bytes([i]) + context
|
|
||||||
dk += prf(base_key, info)
|
|
||||||
i += 1
|
|
||||||
if i > 0xffff:
|
|
||||||
raise ValueError("Overflow in SP800 108 counter")
|
|
||||||
return dk[:output_len]
|
|
||||||
|
|
||||||
|
|
||||||
class Scp03SessionKeys:
|
|
||||||
# GPC 2.3 Amendment D v1.2 Section 4.1.5 Table 4-1
|
|
||||||
DERIV_CONST_AUTH_CGRAM_CARD = b'\x00'
|
|
||||||
DERIV_CONST_AUTH_CGRAM_HOST = b'\x01'
|
|
||||||
DERIV_CONST_CARD_CHLG_GEN = b'\x02'
|
|
||||||
DERIV_CONST_KDERIV_S_ENC = b'\x04'
|
|
||||||
DERIV_CONST_KDERIV_S_MAC = b'\x06'
|
|
||||||
DERIV_CONST_KDERIV_S_RMAC = b'\x07'
|
|
||||||
blocksize = 16
|
|
||||||
|
|
||||||
def __init__(self, card_keys: 'GpCardKeyset', host_challenge: bytes, card_challenge: bytes):
|
|
||||||
# GPC 2.3 Amendment D v1.2 Section 6.2.1
|
|
||||||
context = host_challenge + card_challenge
|
|
||||||
self.s_enc = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_ENC, context, card_keys.enc)
|
|
||||||
self.s_mac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_MAC, context, card_keys.mac)
|
|
||||||
self.s_rmac = scp03_key_derivation(self.DERIV_CONST_KDERIV_S_RMAC, context, card_keys.mac)
|
|
||||||
|
|
||||||
|
|
||||||
# The first MAC chaining value is set to 16 bytes '00'
|
|
||||||
self.mac_chaining_value = b'\x00' * 16
|
|
||||||
# The encryption counter’s start value shall be set to 1 (we set it immediately before generating ICV)
|
|
||||||
self.block_nr = 0
|
|
||||||
|
|
||||||
def calc_cmac(self, apdu: bytes):
|
|
||||||
"""Compute C-MAC for given to-be-transmitted APDU.
|
|
||||||
Returns the full 16-byte MAC, caller must truncate it if needed for S8 mode."""
|
|
||||||
cmac_input = self.mac_chaining_value + apdu
|
|
||||||
cmac_val = CMAC.new(self.s_mac, cmac_input, ciphermod=AES).digest()
|
|
||||||
self.mac_chaining_value = cmac_val
|
|
||||||
return cmac_val
|
|
||||||
|
|
||||||
def calc_rmac(self, rdata_and_sw: bytes):
|
|
||||||
"""Compute R-MAC for given received R-APDU data section.
|
|
||||||
Returns the full 16-byte MAC, caller must truncate it if needed for S8 mode."""
|
|
||||||
rmac_input = self.mac_chaining_value + rdata_and_sw
|
|
||||||
return CMAC.new(self.s_rmac, rmac_input, ciphermod=AES).digest()
|
|
||||||
|
|
||||||
def _get_icv(self, is_response: bool = False):
|
|
||||||
"""Obtain the ICV value computed as described in 6.2.6.
|
|
||||||
This method has two modes:
|
|
||||||
* is_response=False for computing the ICV for C-ENC. Will pre-increment the counter.
|
|
||||||
* is_response=False for computing the ICV for R-DEC."""
|
|
||||||
if not is_response:
|
|
||||||
self.block_nr += 1
|
|
||||||
# The binary value of this number SHALL be left padded with zeroes to form a full block.
|
|
||||||
data = self.block_nr.to_bytes(self.blocksize, "big")
|
|
||||||
if is_response:
|
|
||||||
# Section 6.2.7: additional intermediate step: Before encryption, the most significant byte of
|
|
||||||
# this block shall be set to '80'.
|
|
||||||
data = b'\x80' + data[1:]
|
|
||||||
iv = bytes([0] * self.blocksize)
|
|
||||||
# This block SHALL be encrypted with S-ENC to produce the ICV for command encryption.
|
|
||||||
cipher = AES.new(self.s_enc, AES.MODE_CBC, iv)
|
|
||||||
icv = cipher.encrypt(data)
|
|
||||||
logger.debug("_get_icv(data=%s, is_resp=%s) -> icv=%s", b2h(data), is_response, b2h(icv))
|
|
||||||
return icv
|
|
||||||
|
|
||||||
# TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-wrapping
|
|
||||||
def _encrypt(self, data: bytes, is_response: bool = False) -> bytes:
|
|
||||||
cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv(is_response))
|
|
||||||
return cipher.encrypt(data)
|
|
||||||
|
|
||||||
# TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-unwrapping
|
|
||||||
def _decrypt(self, data: bytes, is_response: bool = True) -> bytes:
|
|
||||||
cipher = AES.new(self.s_enc, AES.MODE_CBC, self._get_icv(is_response))
|
|
||||||
return cipher.decrypt(data)
|
|
||||||
|
|
||||||
|
|
||||||
class SCP03(SCP):
|
|
||||||
"""Secure Channel Protocol (SCP) 03 as specified in GlobalPlatform v2.3 Amendment D."""
|
|
||||||
|
|
||||||
# Section 7.1.1.6 / Table 7-3
|
|
||||||
constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x03'), 'i_param'/Int8ub,
|
|
||||||
'card_challenge'/Bytes(lambda ctx: ctx._.s_mode),
|
|
||||||
'card_cryptogram'/Bytes(lambda ctx: ctx._.s_mode),
|
|
||||||
'sequence_counter'/COptional(Bytes(3)))
|
|
||||||
kvn_range = [0x30, 0x3f]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.s_mode = kwargs.pop('s_mode', 8)
|
|
||||||
self.overhead = self.s_mode
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
|
||||||
cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
|
|
||||||
return cipher.encrypt(plaintext)
|
|
||||||
|
|
||||||
def dek_decrypt(self, ciphertext:bytes) -> bytes:
|
|
||||||
cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
|
|
||||||
return cipher.decrypt(ciphertext)
|
|
||||||
|
|
||||||
def _compute_cryptograms(self):
|
|
||||||
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(self.host_challenge), b2h(self.card_challenge))
|
|
||||||
# Card + Host Authentication Cryptogram: Section 6.2.2.2 + 6.2.2.3
|
|
||||||
context = self.host_challenge + self.card_challenge
|
|
||||||
self.card_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_CARD, context, self.sk.s_mac, l=self.s_mode*8)
|
|
||||||
self.host_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_HOST, context, self.sk.s_mac, l=self.s_mode*8)
|
|
||||||
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
|
|
||||||
|
|
||||||
def gen_init_update_apdu(self, host_challenge: Optional[bytes] = None) -> bytes:
|
|
||||||
"""Generate INITIALIZE UPDATE APDU."""
|
|
||||||
if host_challenge is None:
|
|
||||||
host_challenge = b'\x00' * self.s_mode
|
|
||||||
if len(host_challenge) != self.s_mode:
|
|
||||||
raise ValueError('Host Challenge must be %u bytes long' % self.s_mode)
|
|
||||||
self.host_challenge = host_challenge
|
|
||||||
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge + b'\x00'
|
|
||||||
|
|
||||||
def parse_init_update_resp(self, resp_bin: bytes):
|
|
||||||
"""Parse response to INITIALIZE UPDATE."""
|
|
||||||
if len(resp_bin) not in [10+3+8+8, 10+3+16+16, 10+3+8+8+3, 10+3+16+16+3]:
|
|
||||||
raise ValueError('Invalid length of Initialize Update Response')
|
|
||||||
resp = self.constr_iur.parse(resp_bin, s_mode=self.s_mode)
|
|
||||||
self.card_challenge = resp['card_challenge']
|
|
||||||
self.i_param = resp['i_param']
|
|
||||||
# derive session keys and compute cryptograms
|
|
||||||
self.sk = Scp03SessionKeys(self.card_keys, self.host_challenge, self.card_challenge)
|
|
||||||
logger.debug(self.sk)
|
|
||||||
self._compute_cryptograms()
|
|
||||||
# verify computed cryptogram matches received cryptogram
|
|
||||||
if self.card_cryptogram != resp['card_cryptogram']:
|
|
||||||
raise ValueError("card cryptogram doesn't match")
|
|
||||||
|
|
||||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
|
||||||
"""Generate EXTERNAL AUTHENTICATE APDU."""
|
|
||||||
self.security_level = security_level
|
|
||||||
header = bytes([self._cla(), INS_EXT_AUTH, self.security_level, 0, self.s_mode])
|
|
||||||
# bypass encryption for EXTERNAL AUTHENTICATE
|
|
||||||
return self.wrap_cmd_apdu(header + self.host_cryptogram, skip_cenc=True)
|
|
||||||
|
|
||||||
def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes:
|
|
||||||
"""Wrap Command APDU for SCP03: calculate MAC and encrypt."""
|
|
||||||
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
|
||||||
|
|
||||||
if not self.do_cmac:
|
|
||||||
return apdu
|
|
||||||
|
|
||||||
cla = apdu[0]
|
|
||||||
ins = apdu[1]
|
|
||||||
p1 = apdu[2]
|
|
||||||
p2 = apdu[3]
|
|
||||||
(case, lc, le, cmd_data) = parse_command_apdu(apdu)
|
|
||||||
|
|
||||||
# TODO: add support for extended length fields.
|
|
||||||
assert lc <= 256
|
|
||||||
assert le <= 256
|
|
||||||
lc &= 0xFF
|
|
||||||
le &= 0xFF
|
|
||||||
|
|
||||||
if self.do_cenc and not skip_cenc:
|
|
||||||
if case <= 2:
|
|
||||||
# No encryption shall be applied to a command where there is no command data field. In this
|
|
||||||
# case, the encryption counter shall still be incremented
|
|
||||||
self.sk.block_nr += 1
|
|
||||||
else:
|
|
||||||
# data shall be padded as defined in [GPCS] section B.2.3
|
|
||||||
padded_data = pad80(cmd_data, 16)
|
|
||||||
lc = len(padded_data)
|
|
||||||
if lc >= 256:
|
|
||||||
raise ValueError('Modified Lc (%u) would exceed maximum when appending padding' % (lc))
|
|
||||||
# perform AES-CBC with ICV + S_ENC
|
|
||||||
cmd_data = self.sk._encrypt(padded_data)
|
|
||||||
|
|
||||||
# The length of the command message (Lc) shall be incremented by 8 (in S8 mode) or 16 (in S16
|
|
||||||
# mode) to indicate the inclusion of the C-MAC in the data field of the command message.
|
|
||||||
mlc = lc + self.s_mode
|
|
||||||
if mlc >= 256:
|
|
||||||
raise ValueError('Modified Lc (%u) would exceed maximum when appending %u bytes of mac' % (mlc, self.s_mode))
|
|
||||||
# The class byte shall be modified for the generation or verification of the C-MAC: The logical
|
|
||||||
# channel number shall be set to zero, bit 4 shall be set to 0 and bit 3 shall be set to 1 to indicate
|
|
||||||
# GlobalPlatform proprietary secure messaging.
|
|
||||||
mcla = (cla & 0xF0) | CLA_SM
|
|
||||||
apdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
|
|
||||||
cmac = self.sk.calc_cmac(apdu)
|
|
||||||
apdu += cmac[:self.s_mode]
|
|
||||||
|
|
||||||
# See comment in SCP03._wrap_cmd_apdu()
|
|
||||||
if case == 4 or case == 2:
|
|
||||||
apdu += b'\x00'
|
|
||||||
|
|
||||||
return apdu
|
|
||||||
|
|
||||||
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
|
|
||||||
# No R-MAC shall be generated and no protection shall be applied to a response that includes an error
|
|
||||||
# status word: in this case only the status word shall be returned in the response. All status words
|
|
||||||
# except '9000' and warning status words (i.e. '62xx' and '63xx') shall be interpreted as error status
|
|
||||||
# words.
|
|
||||||
logger.debug("unwrap_rsp_apdu(sw=%s, rsp_apdu=%s)", sw, rsp_apdu)
|
|
||||||
if not self.do_rmac:
|
|
||||||
assert not self.do_renc
|
|
||||||
return rsp_apdu
|
|
||||||
|
|
||||||
if sw != b'\x90\x00' and sw[0] not in [0x62, 0x63]:
|
|
||||||
return rsp_apdu
|
|
||||||
response_data = rsp_apdu[:-self.s_mode]
|
|
||||||
rmac = rsp_apdu[-self.s_mode:]
|
|
||||||
rmac_exp = self.sk.calc_rmac(response_data + sw)[:self.s_mode]
|
|
||||||
if rmac != rmac_exp:
|
|
||||||
raise ValueError("R-MAC value not matching: received: %s, computed: %s" % (rmac, rmac_exp))
|
|
||||||
|
|
||||||
if self.do_renc:
|
|
||||||
# decrypt response data
|
|
||||||
decrypted = self.sk._decrypt(response_data)
|
|
||||||
logger.debug("decrypted: %s", b2h(decrypted))
|
|
||||||
# remove padding
|
|
||||||
response_data = unpad80(decrypted)
|
|
||||||
logger.debug("response_data: %s", b2h(response_data))
|
|
||||||
|
|
||||||
return response_data
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# coding=utf-8
|
|
||||||
"""GlobalPLatform UICC Configuration 1.0 parameters
|
|
||||||
|
|
||||||
(C) 2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from construct import Optional as COptional
|
|
||||||
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
|
|
||||||
from osmocom.construct import *
|
|
||||||
from osmocom.utils import *
|
|
||||||
from osmocom.tlv import *
|
|
||||||
|
|
||||||
# Section 11.6.2.3 / Table 11-58
|
|
||||||
class SecurityDomainAid(BER_TLV_IE, tag=0x4f):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
class LoadFileDataBlockSignature(BER_TLV_IE, tag=0xc3):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
class DapBlock(BER_TLV_IE, tag=0xe2, nested=[SecurityDomainAid, LoadFileDataBlockSignature]):
|
|
||||||
pass
|
|
||||||
class LoadFileDataBlock(BER_TLV_IE, tag=0xc4):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
class Icv(BER_TLV_IE, tag=0xd3):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
class CipheredLoadFileDataBlock(BER_TLV_IE, tag=0xd4):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
class LoadFile(TLV_IE_Collection, nested=[DapBlock, LoadFileDataBlock, Icv, CipheredLoadFileDataBlock]):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# UICC Configuration v1.0.1 / Section 4.3.2
|
|
||||||
class UiccScp(BER_TLV_IE, tag=0x81):
|
|
||||||
_construct = Struct('scp'/Int8ub, 'i'/Int8ub)
|
|
||||||
|
|
||||||
class AcceptExtradAppsAndElfToSd(BER_TLV_IE, tag=0x82):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
|
|
||||||
class AcceptDelOfAssocSd(BER_TLV_IE, tag=0x83):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
|
|
||||||
class LifeCycleTransitionToPersonalized(BER_TLV_IE, tag=0x84):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
|
|
||||||
class CasdCapabilityInformation(BER_TLV_IE, tag=0x86):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
|
|
||||||
class AcceptExtradAssocAppsAndElf(BER_TLV_IE, tag=0x87):
|
|
||||||
_construct = GreedyBytes
|
|
||||||
|
|
||||||
# Security Domain Install Parameters (inside C9 during INSTALL [for install])
|
|
||||||
class UiccSdInstallParams(TLV_IE_Collection, nested=[UiccScp, AcceptExtradAppsAndElfToSd, AcceptDelOfAssocSd,
|
|
||||||
LifeCycleTransitionToPersonalized,
|
|
||||||
CasdCapabilityInformation, AcceptExtradAssocAppsAndElf]):
|
|
||||||
def has_scp(self, scp: int) -> bool:
|
|
||||||
"""Determine if SD Installation parameters already specify given SCP."""
|
|
||||||
for c in self.children:
|
|
||||||
if not isinstance(c, UiccScp):
|
|
||||||
continue
|
|
||||||
if c.decoded['scp'] == scp:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_scp(self, scp: int, i: int):
|
|
||||||
"""Add given SCP (and i parameter) to list of SCP of the Security Domain Install Params.
|
|
||||||
Example: add_scp(0x03, 0x70) for SCP03, or add_scp(0x02, 0x55) for SCP02."""
|
|
||||||
if self.has_scp(scp):
|
|
||||||
raise ValueError('SCP%02x already present' % scp)
|
|
||||||
self.children.append(UiccScp(decoded={'scp': scp, 'i': i}))
|
|
||||||
|
|
||||||
def remove_scp(self, scp: int):
|
|
||||||
"""Remove given SCP from list of SCP of the Security Domain Install Params."""
|
|
||||||
for c in self.children:
|
|
||||||
if not isinstance(c, UiccScp):
|
|
||||||
continue
|
|
||||||
if c.decoded['scp'] == scp:
|
|
||||||
self.children.remove(c)
|
|
||||||
return
|
|
||||||
raise ValueError("SCP%02x not present" % scp)
|
|
||||||
|
|
||||||
|
|
||||||
# Key Usage:
|
|
||||||
# KVN 0x01 .. 0x0F reserved for SCP80
|
|
||||||
# KVN 0x11 reserved for DAP specified in ETSI TS 102 226
|
|
||||||
# KVN 0x20 .. 0x2F reserved for SCP02
|
|
||||||
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
|
|
||||||
# KVN 0x30 .. 0x3F reserved for SCP03
|
|
||||||
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK
|
|
||||||
# KVN 0x70 KID 0x01: Token key (RSA public or DES)
|
|
||||||
# KVN 0x71 KID 0x01: Receipt key (DES)
|
|
||||||
# KVN 0x73 KID 0x01: DAP verifiation key (RS public or DES)
|
|
||||||
# KVN 0x74 reserved for CASD
|
|
||||||
# KID 0x01: PK.CA.AUT
|
|
||||||
# KID 0x02: SK.CASD.AUT (PK) and KS.CASD.AUT (Non-PK)
|
|
||||||
# KID 0x03: SK.CASD.CT (P) and KS.CASD.CT (Non-PK)
|
|
||||||
# KVN 0x75 KID 0x01: 16-byte DES key for Ciphered Load File Data Block
|
|
||||||
# KVN 0xFF reserved for ISD with SCP02 without SCP80 s support
|
|
||||||
104
pySim/gsm_r.py
104
pySim/gsm_r.py
@@ -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
|
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"
|
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>
|
# 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.utils import *
|
||||||
|
#from pySim.tlv import *
|
||||||
from struct import pack, unpack
|
from struct import pack, unpack
|
||||||
from construct import Struct, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
|
from construct import *
|
||||||
from construct import Optional as COptional
|
from construct import Optional as COptional
|
||||||
from osmocom.construct import *
|
from pySim.construct import *
|
||||||
|
import enum
|
||||||
|
|
||||||
from pySim.profile import CardProfileAddon
|
from pySim.profile import CardProfileAddon
|
||||||
from pySim.filesystem import *
|
from pySim.filesystem import *
|
||||||
|
import pySim.ts_51_011
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# DF.EIRENE (FFFIS for GSM-R SIM Cards)
|
# DF.EIRENE (FFFIS for GSM-R SIM Cards)
|
||||||
@@ -71,15 +77,15 @@ class PlConfAdapter(Adapter):
|
|||||||
num = int(obj) & 0x7
|
num = int(obj) & 0x7
|
||||||
if num == 0:
|
if num == 0:
|
||||||
return 'None'
|
return 'None'
|
||||||
if num == 1:
|
elif num == 1:
|
||||||
return 4
|
return 4
|
||||||
if num == 2:
|
elif num == 2:
|
||||||
return 3
|
return 3
|
||||||
if num == 3:
|
elif num == 3:
|
||||||
return 2
|
return 2
|
||||||
if num == 4:
|
elif num == 4:
|
||||||
return 1
|
return 1
|
||||||
if num == 5:
|
elif num == 5:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def _encode(self, obj, context, path):
|
def _encode(self, obj, context, path):
|
||||||
@@ -88,13 +94,13 @@ class PlConfAdapter(Adapter):
|
|||||||
obj = int(obj)
|
obj = int(obj)
|
||||||
if obj == 4:
|
if obj == 4:
|
||||||
return 1
|
return 1
|
||||||
if obj == 3:
|
elif obj == 3:
|
||||||
return 2
|
return 2
|
||||||
if obj == 2:
|
elif obj == 2:
|
||||||
return 3
|
return 3
|
||||||
if obj == 1:
|
elif obj == 1:
|
||||||
return 4
|
return 4
|
||||||
if obj == 0:
|
elif obj == 0:
|
||||||
return 5
|
return 5
|
||||||
|
|
||||||
|
|
||||||
@@ -105,19 +111,19 @@ class PlCallAdapter(Adapter):
|
|||||||
num = int(obj) & 0x7
|
num = int(obj) & 0x7
|
||||||
if num == 0:
|
if num == 0:
|
||||||
return 'None'
|
return 'None'
|
||||||
if num == 1:
|
elif num == 1:
|
||||||
return 4
|
return 4
|
||||||
if num == 2:
|
elif num == 2:
|
||||||
return 3
|
return 3
|
||||||
if num == 3:
|
elif num == 3:
|
||||||
return 2
|
return 2
|
||||||
if num == 4:
|
elif num == 4:
|
||||||
return 1
|
return 1
|
||||||
if num == 5:
|
elif num == 5:
|
||||||
return 0
|
return 0
|
||||||
if num == 6:
|
elif num == 6:
|
||||||
return 'B'
|
return 'B'
|
||||||
if num == 7:
|
elif num == 7:
|
||||||
return 'A'
|
return 'A'
|
||||||
|
|
||||||
def _encode(self, obj, context, path):
|
def _encode(self, obj, context, path):
|
||||||
@@ -125,17 +131,17 @@ class PlCallAdapter(Adapter):
|
|||||||
return 0
|
return 0
|
||||||
if obj == 4:
|
if obj == 4:
|
||||||
return 1
|
return 1
|
||||||
if obj == 3:
|
elif obj == 3:
|
||||||
return 2
|
return 2
|
||||||
if obj == 2:
|
elif obj == 2:
|
||||||
return 3
|
return 3
|
||||||
if obj == 1:
|
elif obj == 1:
|
||||||
return 4
|
return 4
|
||||||
if obj == 0:
|
elif obj == 0:
|
||||||
return 5
|
return 5
|
||||||
if obj == 'B':
|
elif obj == 'B':
|
||||||
return 6
|
return 6
|
||||||
if obj == 'A':
|
elif obj == 'A':
|
||||||
return 7
|
return 7
|
||||||
|
|
||||||
|
|
||||||
@@ -184,13 +190,13 @@ class EF_CallconfI(LinFixedEF):
|
|||||||
class EF_Shunting(TransparentEF):
|
class EF_Shunting(TransparentEF):
|
||||||
"""Section 7.6"""
|
"""Section 7.6"""
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": h2b("f8ffffff000000") } ),
|
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": "f8ffffff000000" } ),
|
||||||
]
|
]
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(fid='6ff4', sfid=None,
|
super().__init__(fid='6ff4', sfid=None,
|
||||||
name='EF.Shunting', desc='Shunting', size=(8, 8))
|
name='EF.Shunting', desc='Shunting', size=(8, 8))
|
||||||
self._construct = Struct('common_gid'/Int8ub,
|
self._construct = Struct('common_gid'/Int8ub,
|
||||||
'shunting_gid'/Bytes(7))
|
'shunting_gid'/HexAdapter(Bytes(7)))
|
||||||
|
|
||||||
|
|
||||||
class EF_GsmrPLMN(LinFixedEF):
|
class EF_GsmrPLMN(LinFixedEF):
|
||||||
@@ -199,13 +205,13 @@ class EF_GsmrPLMN(LinFixedEF):
|
|||||||
( "22f860f86f8d6f8e01", { "plmn": "228-06", "class_of_network": {
|
( "22f860f86f8d6f8e01", { "plmn": "228-06", "class_of_network": {
|
||||||
"supported": { "vbs": True, "vgcs": True, "emlpp": True,
|
"supported": { "vbs": True, "vgcs": True, "emlpp": True,
|
||||||
"fn": True, "eirene": True }, "preference": 0 },
|
"fn": True, "eirene": True }, "preference": 0 },
|
||||||
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
|
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
|
||||||
"ic_table_ref": h2b("01") } ),
|
"ic_table_ref": "01" } ),
|
||||||
( "22f810416f8d6f8e02", { "plmn": "228-01", "class_of_network": {
|
( "22f810416f8d6f8e02", { "plmn": "228-01", "class_of_network": {
|
||||||
"supported": { "vbs": False, "vgcs": False, "emlpp": False,
|
"supported": { "vbs": False, "vgcs": False, "emlpp": False,
|
||||||
"fn": True, "eirene": False }, "preference": 1 },
|
"fn": True, "eirene": False }, "preference": 1 },
|
||||||
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
|
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
|
||||||
"ic_table_ref": h2b("02") } ),
|
"ic_table_ref": "02" } ),
|
||||||
]
|
]
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(fid='6ff5', sfid=None, name='EF.GsmrPLMN',
|
super().__init__(fid='6ff5', sfid=None, name='EF.GsmrPLMN',
|
||||||
@@ -213,24 +219,24 @@ class EF_GsmrPLMN(LinFixedEF):
|
|||||||
self._construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
|
self._construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
|
||||||
'class_of_network'/BitStruct('supported'/FlagsEnum(BitsInteger(5), vbs=1, vgcs=2, emlpp=4, fn=8, eirene=16),
|
'class_of_network'/BitStruct('supported'/FlagsEnum(BitsInteger(5), vbs=1, vgcs=2, emlpp=4, fn=8, eirene=16),
|
||||||
'preference'/BitsInteger(3)),
|
'preference'/BitsInteger(3)),
|
||||||
'ic_incoming_ref_tbl'/Bytes(2),
|
'ic_incoming_ref_tbl'/HexAdapter(Bytes(2)),
|
||||||
'outgoing_ref_tbl'/Bytes(2),
|
'outgoing_ref_tbl'/HexAdapter(Bytes(2)),
|
||||||
'ic_table_ref'/Bytes(1))
|
'ic_table_ref'/HexAdapter(Bytes(1)))
|
||||||
|
|
||||||
|
|
||||||
class EF_IC(LinFixedEF):
|
class EF_IC(LinFixedEF):
|
||||||
"""Section 7.8"""
|
"""Section 7.8"""
|
||||||
_test_de_encode = [
|
_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 } ),
|
"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 } ),
|
"ic_decision_value": "ffff", "network_string_table_index": 65535 } ),
|
||||||
]
|
]
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(fid='6f8d', sfid=None, name='EF.IC',
|
super().__init__(fid='6f8d', sfid=None, name='EF.IC',
|
||||||
desc='International Code', rec_len=(7, 7))
|
desc='International Code', rec_len=(7, 7))
|
||||||
self._construct = Struct('next_table_type'/NextTableType,
|
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)),
|
'ic_decision_value'/BcdAdapter(Bytes(2)),
|
||||||
'network_string_table_index'/Int16ub)
|
'network_string_table_index'/Int16ub)
|
||||||
|
|
||||||
@@ -252,18 +258,18 @@ class EF_NW(LinFixedEF):
|
|||||||
class EF_Switching(LinFixedEF):
|
class EF_Switching(LinFixedEF):
|
||||||
"""Section 8.4"""
|
"""Section 8.4"""
|
||||||
_test_de_encode = [
|
_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 } ),
|
"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 } ),
|
"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 } ),
|
"decision_value": "5fff", "string_table_index": 5 } ),
|
||||||
]
|
]
|
||||||
def __init__(self, fid='1234', name='Switching', desc=None):
|
def __init__(self, fid='1234', name='Switching', desc=None):
|
||||||
super().__init__(fid=fid, sfid=None,
|
super().__init__(fid=fid, sfid=None,
|
||||||
name=name, desc=desc, rec_len=(6, 6))
|
name=name, desc=desc, rec_len=(6, 6))
|
||||||
self._construct = Struct('next_table_type'/NextTableType,
|
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)),
|
'decision_value'/BcdAdapter(Bytes(2)),
|
||||||
'string_table_index'/Int8ub)
|
'string_table_index'/Int8ub)
|
||||||
|
|
||||||
@@ -271,12 +277,12 @@ class EF_Switching(LinFixedEF):
|
|||||||
class EF_Predefined(LinFixedEF):
|
class EF_Predefined(LinFixedEF):
|
||||||
"""Section 8.5"""
|
"""Section 8.5"""
|
||||||
_test_de_encode = [
|
_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 } ),
|
( "f0ffc8", 2, { "predefined_value1": "0fff", "string_table_index1": 200 } ),
|
||||||
]
|
]
|
||||||
# header and other records have different structure. WTF !?!
|
# header and other records have different structure. WTF !?!
|
||||||
construct_first = Struct('next_table_type'/NextTableType,
|
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)),
|
construct_others = Struct('predefined_value1'/BcdAdapter(Bytes(2)),
|
||||||
'string_table_index1'/Int8ub)
|
'string_table_index1'/Int8ub)
|
||||||
|
|
||||||
@@ -290,7 +296,7 @@ class EF_Predefined(LinFixedEF):
|
|||||||
else:
|
else:
|
||||||
return parse_construct(self.construct_others, raw_bin_data)
|
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
|
r = None
|
||||||
if record_nr == 1:
|
if record_nr == 1:
|
||||||
r = self.construct_first.build(abstract_data)
|
r = self.construct_first.build(abstract_data)
|
||||||
@@ -301,13 +307,13 @@ class EF_Predefined(LinFixedEF):
|
|||||||
class EF_DialledVals(TransparentEF):
|
class EF_DialledVals(TransparentEF):
|
||||||
"""Section 8.6"""
|
"""Section 8.6"""
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": h2b("ffff"), "dialed_digits": "22" } ),
|
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": "ffff", "dialed_digits": "22" } ),
|
||||||
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": h2b("6f88"), "dialed_digits": "58" }),
|
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": "6f88", "dialed_digits": "58" }),
|
||||||
]
|
]
|
||||||
def __init__(self, fid='1234', name='DialledVals', desc=None):
|
def __init__(self, fid='1234', name='DialledVals', desc=None):
|
||||||
super().__init__(fid=fid, sfid=None, name=name, desc=desc, size=(4, 4))
|
super().__init__(fid=fid, sfid=None, name=name, desc=desc, size=(4, 4))
|
||||||
self._construct = Struct('next_table_type'/NextTableType,
|
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)))
|
'dialed_digits'/BcdAdapter(Bytes(1)))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
214
pySim/gsmtap.py
Normal file
214
pySim/gsmtap.py
Normal 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
|
||||||
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from construct import GreedyString
|
from construct import *
|
||||||
from osmocom.tlv import *
|
from pySim.construct import *
|
||||||
from osmocom.construct import *
|
from pySim.utils import *
|
||||||
|
from pySim.filesystem import *
|
||||||
|
from pySim.tlv import *
|
||||||
|
|
||||||
# Table 91 + Section 8.2.1.2
|
# Table 91 + Section 8.2.1.2
|
||||||
class ApplicationId(BER_TLV_IE, tag=0x4f):
|
class ApplicationId(BER_TLV_IE, tag=0x4f):
|
||||||
|
|||||||
@@ -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']
|
|
||||||
|
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
import json
|
||||||
|
import pprint
|
||||||
|
import jsonpath_ng
|
||||||
|
|
||||||
"""JSONpath utility functions as needed within pysim.
|
"""JSONpath utility functions as needed within pysim.
|
||||||
|
|
||||||
As pySim-sell has the ability to represent SIM files as JSON strings,
|
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.
|
of a file or record in its JSON representation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import jsonpath_ng
|
|
||||||
|
|
||||||
# (C) 2021 by Harald Welte <laforge@osmocom.org>
|
# (C) 2021 by Harald Welte <laforge@osmocom.org>
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
from smartcard.util import toBytes
|
||||||
from pytlv.TLV import *
|
from pytlv.TLV import *
|
||||||
|
|
||||||
from pySim.cards import SimCardBase, UiccCardBase
|
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 enc_plmn, get_addr_type
|
||||||
from pySim.utils import is_hex, h2b, b2h, h2s, s2h, lpad, rpad
|
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 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_51_011 import EF, DF
|
||||||
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
|
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
|
||||||
from pySim.legacy.ts_31_103 import EF_ISIM_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):
|
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
|
each possible provider uses a specific record number in each EF. The
|
||||||
indexes used are ( where N is the number of providers supported ) :
|
indexes used are ( where N is the number of providers supported ) :
|
||||||
- [2 .. N+1] for the operator name
|
- [2 .. N+1] for the operator name
|
||||||
@@ -644,7 +645,7 @@ class MagicSim(MagicSimBase):
|
|||||||
|
|
||||||
class FakeMagicSim(SimCard):
|
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
|
information. See the program method for its format. The records go from
|
||||||
1 to N.
|
1 to N.
|
||||||
"""
|
"""
|
||||||
@@ -752,7 +753,7 @@ class GrcardSim(SimCard):
|
|||||||
|
|
||||||
# Set the Ki using proprietary command
|
# Set the Ki using proprietary command
|
||||||
pdu = '80d4020010' + p['ki']
|
pdu = '80d4020010' + p['ki']
|
||||||
data, sw = self._scc.send_apdu(pdu)
|
data, sw = self._scc._tp.send_apdu(pdu)
|
||||||
|
|
||||||
# EF.HPLMN
|
# EF.HPLMN
|
||||||
r = self._scc.select_path(['3f00', '7f20', '6f30'])
|
r = self._scc.select_path(['3f00', '7f20', '6f30'])
|
||||||
@@ -780,7 +781,7 @@ class SysmoSIMgr1(GrcardSim):
|
|||||||
def autodetect(kls, scc):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
# Look for ATR
|
# 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)
|
return kls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@@ -802,7 +803,7 @@ class SysmoUSIMgr1(UsimCard):
|
|||||||
# TODO: check if verify_chv could be used or what it needs
|
# TODO: check if verify_chv could be used or what it needs
|
||||||
# self._scc.verify_chv(0x0A, [0x33,0x32,0x32,0x31,0x33,0x32,0x33,0x32])
|
# self._scc.verify_chv(0x0A, [0x33,0x32,0x32,0x31,0x33,0x32,0x33,0x32])
|
||||||
# Unlock the card..
|
# Unlock the card..
|
||||||
data, sw = self._scc.send_apdu_checksw(
|
data, sw = self._scc._tp.send_apdu_checksw(
|
||||||
"0020000A083332323133323332")
|
"0020000A083332323133323332")
|
||||||
|
|
||||||
# TODO: move into SimCardCommands
|
# TODO: move into SimCardCommands
|
||||||
@@ -811,7 +812,7 @@ class SysmoUSIMgr1(UsimCard):
|
|||||||
enc_iccid(p['iccid']) + # 10b ICCID
|
enc_iccid(p['iccid']) + # 10b ICCID
|
||||||
enc_imsi(p['imsi']) # 9b IMSI_len + id_type(9) + IMSI
|
enc_imsi(p['imsi']) # 9b IMSI_len + id_type(9) + IMSI
|
||||||
)
|
)
|
||||||
data, sw = self._scc.send_apdu_checksw("0099000033" + par)
|
data, sw = self._scc._tp.send_apdu_checksw("0099000033" + par)
|
||||||
|
|
||||||
|
|
||||||
class SysmoSIMgr2(SimCard):
|
class SysmoSIMgr2(SimCard):
|
||||||
@@ -825,7 +826,7 @@ class SysmoSIMgr2(SimCard):
|
|||||||
def autodetect(kls, scc):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
# Look for ATR
|
# 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)
|
return kls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@@ -850,7 +851,7 @@ class SysmoSIMgr2(SimCard):
|
|||||||
pin = h2b("4444444444444444")
|
pin = h2b("4444444444444444")
|
||||||
|
|
||||||
pdu = 'A0D43A0508' + b2h(pin)
|
pdu = 'A0D43A0508' + b2h(pin)
|
||||||
data, sw = self._scc.send_apdu(pdu)
|
data, sw = self._scc._tp.send_apdu(pdu)
|
||||||
|
|
||||||
# authenticate as ADM (enough to write file, and can set PINs)
|
# authenticate as ADM (enough to write file, and can set PINs)
|
||||||
|
|
||||||
@@ -903,7 +904,7 @@ class SysmoUSIMSJS1(UsimCard):
|
|||||||
def autodetect(kls, scc):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
# Look for ATR
|
# 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)
|
return kls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@@ -1031,7 +1032,7 @@ class FairwavesSIM(UsimCard):
|
|||||||
def autodetect(kls, scc):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
# Look for ATR
|
# 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)
|
return kls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@@ -1165,7 +1166,7 @@ class OpenCellsSim(SimCard):
|
|||||||
def autodetect(kls, scc):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
# Look for ATR
|
# 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)
|
return kls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@@ -1214,7 +1215,7 @@ class WavemobileSim(UsimCard):
|
|||||||
def autodetect(kls, scc):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
# Look for ATR
|
# 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)
|
return kls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@@ -1304,18 +1305,18 @@ class SysmoISIMSJA2(UsimCard, IsimCard):
|
|||||||
def autodetect(kls, scc):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
# Try card model #1
|
# Try card model #1
|
||||||
atr = "3b9f96801f878031e073fe211b674a4c753034054ba9"
|
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() == atr:
|
if scc.get_atr() == toBytes(atr):
|
||||||
return kls(scc)
|
return kls(scc)
|
||||||
|
|
||||||
# Try card model #2
|
# Try card model #2
|
||||||
atr = "3b9f96801f878031e073fe211b674a4c7531330251b2"
|
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() == atr:
|
if scc.get_atr() == toBytes(atr):
|
||||||
return kls(scc)
|
return kls(scc)
|
||||||
|
|
||||||
# Try card model #3
|
# Try card model #3
|
||||||
atr = "3b9f96801f878031e073fe211b674a4c5275310451d5"
|
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() == atr:
|
if scc.get_atr() == toBytes(atr):
|
||||||
return kls(scc)
|
return kls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@@ -1553,16 +1554,16 @@ class SysmoISIMSJA5(SysmoISIMSJA2):
|
|||||||
def autodetect(kls, scc):
|
def autodetect(kls, scc):
|
||||||
try:
|
try:
|
||||||
# Try card model #1 (9FJ)
|
# Try card model #1 (9FJ)
|
||||||
atr = "3b9f96801f878031e073fe211b674a357530350251cc"
|
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() == atr:
|
if scc.get_atr() == toBytes(atr):
|
||||||
return kls(scc)
|
return kls(scc)
|
||||||
# Try card model #2 (SLM17)
|
# Try card model #2 (SLM17)
|
||||||
atr = "3b9f96801f878031e073fe211b674a357530350265f8"
|
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() == atr:
|
if scc.get_atr() == toBytes(atr):
|
||||||
return kls(scc)
|
return kls(scc)
|
||||||
# Try card model #3 (9FV)
|
# Try card model #3 (9FV)
|
||||||
atr = "3b9f96801f878031e073fe211b674a357530350259c4"
|
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() == atr:
|
if scc.get_atr() == toBytes(atr):
|
||||||
return kls(scc)
|
return kls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
@@ -1591,7 +1592,7 @@ class GialerSim(UsimCard):
|
|||||||
def autodetect(cls, scc):
|
def autodetect(cls, scc):
|
||||||
try:
|
try:
|
||||||
# Look for ATR
|
# 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)
|
return cls(scc)
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -20,10 +20,8 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# 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 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 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):
|
def hexstr_to_Nbytearr(s, nbytes):
|
||||||
return [s[i:i+(nbytes*2)] for i in range(0, len(s), (nbytes*2))]
|
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
|
elif addr_type == 0x01: # IPv4
|
||||||
# Skip address tye byte i.e. first byte in value list
|
# 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:]
|
ipv4 = tlv[2][2:]
|
||||||
content = '.'.join(str(x) for x in ipv4)
|
content = '.'.join(str(x) for x in ipv4)
|
||||||
return (content, '01')
|
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
|
s += '80' + ('%02x' % ((len(ipv4_str)//2)+2)) + '01' + 'ff' + ipv4_str
|
||||||
|
|
||||||
return s
|
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)
|
|
||||||
|
|||||||
128
pySim/log.py
128
pySim/log.py
@@ -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 -- %(name)s - " + 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 type(color) is 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
|
|
||||||
105
pySim/ota.py
105
pySim/ota.py
@@ -1,6 +1,6 @@
|
|||||||
"""Code related to SIM/UICC OTA according to TS 102 225 + TS 31.115."""
|
"""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
|
# 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
|
# 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
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from pySim.construct import *
|
||||||
|
from pySim.utils import b2h
|
||||||
|
from pySim.sms import UserDataHeader
|
||||||
|
from construct import *
|
||||||
import zlib
|
import zlib
|
||||||
import abc
|
import abc
|
||||||
import struct
|
import struct
|
||||||
from typing import Optional, Tuple
|
from typing import Optional
|
||||||
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
|
|
||||||
|
|
||||||
# ETS TS 102 225 gives the general command structure and the dialects for CAT_TP, TCP/IP and HTTPS
|
# 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
|
# 3GPP TS 31.115 gives the dialects for SMS-PP, SMS-CB, USSD and HTTP
|
||||||
@@ -99,17 +97,6 @@ SmsCommandPacket = Struct('cmd_pkt_len'/Int16ub,
|
|||||||
'tar'/Bytes(3),
|
'tar'/Bytes(3),
|
||||||
'secured_data'/GreedyBytes)
|
'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:
|
class OtaKeyset:
|
||||||
"""The OTA related data (key material, counter) to be used in encrypt/decrypt."""
|
"""The OTA related data (key material, counter) to be used in encrypt/decrypt."""
|
||||||
def __init__(self, algo_crypt: str, kic_idx: int, kic: bytes,
|
def __init__(self, algo_crypt: str, kic_idx: int, kic: bytes,
|
||||||
@@ -125,12 +112,12 @@ class OtaKeyset:
|
|||||||
@property
|
@property
|
||||||
def auth(self):
|
def auth(self):
|
||||||
"""Return an instance of the matching OtaAlgoAuth."""
|
"""Return an instance of the matching OtaAlgoAuth."""
|
||||||
return OtaAlgoAuth.from_keyset(self)
|
return OtaAlgoAuth.fromKeyset(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crypt(self):
|
def crypt(self):
|
||||||
"""Return an instance of the matching OtaAlgoCrypt."""
|
"""Return an instance of the matching OtaAlgoCrypt."""
|
||||||
return OtaAlgoCrypt.from_keyset(self)
|
return OtaAlgoCrypt.fromKeyset(self)
|
||||||
|
|
||||||
class OtaCheckError(Exception):
|
class OtaCheckError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -141,24 +128,26 @@ class OtaDialect(abc.ABC):
|
|||||||
def _compute_sig_len(self, spi:SPI):
|
def _compute_sig_len(self, spi:SPI):
|
||||||
if spi['rc_cc_ds'] == 'no_rc_cc_ds':
|
if spi['rc_cc_ds'] == 'no_rc_cc_ds':
|
||||||
return 0
|
return 0
|
||||||
if spi['rc_cc_ds'] == 'rc': # CRC-32
|
elif spi['rc_cc_ds'] == 'rc': # CRC-32
|
||||||
return 4
|
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
|
# TODO: this is not entirely correct, as in AES case it could be 4 or 8
|
||||||
return 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
|
@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
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@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
|
"""Decode a response into a response packet and, if indicted (by a
|
||||||
response status of `"por_ok"`) a decoded response.
|
response status of `"por_ok"`) a decoded response.
|
||||||
|
|
||||||
The response packet's common characteristics are not fully determined,
|
The response packet's common characteristics are not fully determined,
|
||||||
and (so far) completely proprietary per dialect."""
|
and (so far) completely proprietary per dialect."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
from Cryptodome.Cipher import DES, DES3, AES
|
from Cryptodome.Cipher import DES, DES3, AES
|
||||||
@@ -201,7 +190,7 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
|
|||||||
def encrypt(self, data:bytes) -> bytes:
|
def encrypt(self, data:bytes) -> bytes:
|
||||||
"""Encrypt given input bytes using the key material given in constructor."""
|
"""Encrypt given input bytes using the key material given in constructor."""
|
||||||
padded_data = self.pad_to_blocksize(data)
|
padded_data = self.pad_to_blocksize(data)
|
||||||
return self._encrypt(padded_data)
|
return self._encrypt(data)
|
||||||
|
|
||||||
def decrypt(self, data:bytes) -> bytes:
|
def decrypt(self, data:bytes) -> bytes:
|
||||||
"""Decrypt given input bytes using the key material given in constructor."""
|
"""Decrypt given input bytes using the key material given in constructor."""
|
||||||
@@ -210,13 +199,15 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _encrypt(self, data:bytes) -> bytes:
|
def _encrypt(self, data:bytes) -> bytes:
|
||||||
"""Actual implementation, to be implemented by derived class."""
|
"""Actual implementation, to be implemented by derived class."""
|
||||||
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def _decrypt(self, data:bytes) -> bytes:
|
def _decrypt(self, data:bytes) -> bytes:
|
||||||
"""Actual implementation, to be implemented by derived class."""
|
"""Actual implementation, to be implemented by derived class."""
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Resolve the class for the encryption algorithm of otak and instantiate it."""
|
||||||
for subc in cls.__subclasses__():
|
for subc in cls.__subclasses__():
|
||||||
if subc.enum_name == otak.algo_crypt:
|
if subc.enum_name == otak.algo_crypt:
|
||||||
@@ -248,7 +239,7 @@ class OtaAlgoAuth(OtaAlgo, abc.ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Resolve the class for the authentication algorithm of otak and instantiate it."""
|
||||||
for subc in cls.__subclasses__():
|
for subc in cls.__subclasses__():
|
||||||
if subc.enum_name == otak.algo_auth:
|
if subc.enum_name == otak.algo_auth:
|
||||||
@@ -333,7 +324,6 @@ class OtaDialectSms(OtaDialect):
|
|||||||
'response_status'/ResponseStatus,
|
'response_status'/ResponseStatus,
|
||||||
'cc_rc'/Bytes(this.rhl-10),
|
'cc_rc'/Bytes(this.rhl-10),
|
||||||
'secured_data'/GreedyBytes)
|
'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:
|
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
||||||
# length of signature in octets
|
# length of signature in octets
|
||||||
@@ -344,7 +334,6 @@ class OtaDialectSms(OtaDialect):
|
|||||||
len_cipher = 6 + len_sig + len(apdu)
|
len_cipher = 6 + len_sig + len(apdu)
|
||||||
padding = otak.crypt._get_padding(len_cipher, otak.crypt.blocksize)
|
padding = otak.crypt._get_padding(len_cipher, otak.crypt.blocksize)
|
||||||
pad_cnt = len(padding)
|
pad_cnt = len(padding)
|
||||||
apdu = bytes(apdu) # make a copy so we don't modify the input data
|
|
||||||
apdu += padding
|
apdu += padding
|
||||||
|
|
||||||
kic = {'key': otak.kic_idx, 'algo': otak.algo_crypt}
|
kic = {'key': otak.kic_idx, 'algo': otak.algo_crypt}
|
||||||
@@ -355,7 +344,8 @@ class OtaDialectSms(OtaDialect):
|
|||||||
chl = 13 + len_sig
|
chl = 13 + len_sig
|
||||||
|
|
||||||
# CHL + SPI (+ KIC + KID)
|
# 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))
|
#print("part_head: %s" % b2h(part_head))
|
||||||
|
|
||||||
# CNTR + PCNTR (CNTR not used)
|
# CNTR + PCNTR (CNTR not used)
|
||||||
@@ -397,55 +387,8 @@ class OtaDialectSms(OtaDialect):
|
|||||||
|
|
||||||
#print("envelope_data: %s" % b2h(envelope_data))
|
#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
|
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"]):
|
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes) -> ("OtaDialectSms.SmsResponsePacket", Optional["CompactRemoteResp"]):
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = h2b(data)
|
data = h2b(data)
|
||||||
@@ -456,7 +399,7 @@ class OtaDialectSms(OtaDialect):
|
|||||||
# POR with CC+CIPH: 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
|
# POR with CC+CIPH: 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
|
||||||
if data[0] != 0x02:
|
if data[0] != 0x02:
|
||||||
raise ValueError('Unexpected UDL=0x%02x' % data[0])
|
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):
|
if not udhd.has_ie(0x71):
|
||||||
raise ValueError('RPI 0x71 not found in UDH')
|
raise ValueError('RPI 0x71 not found in UDH')
|
||||||
rph_rhl_tar = remainder[:6] # RPH+RHL+TAR; not ciphered
|
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'])
|
raise OtaCheckError('Unknown por_rc_cc_ds: %s' % spi['por_rc_cc_ds'])
|
||||||
|
|
||||||
# TODO: ExpandedRemoteResponse according to TS 102 226 5.2.2
|
# 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'])
|
dec = CompactRemoteResp.parse(res['secured_data'])
|
||||||
else:
|
else:
|
||||||
dec = None
|
dec = None
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -21,14 +21,57 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# 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 abc
|
||||||
import operator
|
import operator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from pySim.exceptions import SwMatchError
|
|
||||||
from pySim.commands import SimCardCommands
|
def _mf_select_test(scc: SimCardCommands,
|
||||||
from pySim.filesystem import CardApplication, interpret_sw
|
cla_byte: str, sel_ctrl: str,
|
||||||
from pySim.utils import all_subclasses
|
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:
|
class CardProfile:
|
||||||
"""A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
|
"""A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
|
||||||
@@ -95,36 +138,10 @@ class CardProfile:
|
|||||||
return data_hex
|
return data_hex
|
||||||
|
|
||||||
@staticmethod
|
@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
|
@abc.abstractmethod
|
||||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
def match_with_card(scc: SimCardCommands) -> bool:
|
||||||
"""Try to see if the specific profile matches the card. This method is a
|
"""Check if the specific profile matches the card. This method is a
|
||||||
placeholder that is overloaded by specific derived classes. The method
|
placeholder that is overloaded by specific dirived classes. The method
|
||||||
actively probes the card to make sure the profile class matches the
|
|
||||||
physical card. This usually also means that the card is reset during
|
|
||||||
the process, so this method must not be called at random times. It may
|
|
||||||
only be called on startup. If there is no exception raised, we assume
|
|
||||||
the card matches the profile.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scc: SimCardCommands class
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def match_with_card(cls, scc: SimCardCommands) -> bool:
|
|
||||||
"""Check if the specific profile matches the card. The method
|
|
||||||
actively probes the card to make sure the profile class matches the
|
actively probes the card to make sure the profile class matches the
|
||||||
physical card. This usually also means that the card is reset during
|
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
|
the process, so this method must not be called at random times. It may
|
||||||
@@ -135,17 +152,7 @@ class CardProfile:
|
|||||||
Returns:
|
Returns:
|
||||||
match = True, no match = False
|
match = True, no match = False
|
||||||
"""
|
"""
|
||||||
sel_backup = scc.sel_ctrl
|
return False
|
||||||
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()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pick(scc: SimCardCommands):
|
def pick(scc: SimCardCommands):
|
||||||
@@ -159,7 +166,7 @@ class CardProfile:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def add_addon(self, addon: 'CardProfileAddon'):
|
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.
|
# we don't install any additional files, as that is happening in the RuntimeState.
|
||||||
self.addons.append(addon)
|
self.addons.append(addon)
|
||||||
|
|
||||||
@@ -179,6 +186,7 @@ class CardProfileAddon(abc.ABC):
|
|||||||
self.desc = kw.get("desc", None)
|
self.desc = kw.get("desc", None)
|
||||||
self.files_in_mf = kw.get("files_in_mf", [])
|
self.files_in_mf = kw.get("files_in_mf", [])
|
||||||
self.shell_cmdsets = kw.get("shell_cmdsets", [])
|
self.shell_cmdsets = kw.get("shell_cmdsets", [])
|
||||||
|
pass
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -186,3 +194,4 @@ class CardProfileAddon(abc.ABC):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def probe(self, card: 'CardBase') -> bool:
|
def probe(self, card: 'CardBase') -> bool:
|
||||||
"""Probe a given card to determine whether or not this add-on is present/supported."""
|
"""Probe a given card to determine whether or not this add-on is present/supported."""
|
||||||
|
pass
|
||||||
|
|||||||
234
pySim/runtime.py
234
pySim/runtime.py
@@ -18,14 +18,10 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from osmocom.utils import h2b, i2h, is_hex, Hexstr
|
|
||||||
from osmocom.tlv import bertlv_parse_one
|
|
||||||
|
|
||||||
|
from pySim.utils import sw_match, h2b, i2h, is_hex, bertlv_parse_one, Hexstr
|
||||||
from pySim.exceptions import *
|
from pySim.exceptions import *
|
||||||
from pySim.filesystem import *
|
from pySim.filesystem import *
|
||||||
from pySim.log import PySimLogger
|
|
||||||
|
|
||||||
log = PySimLogger.get("RUNTIME")
|
|
||||||
|
|
||||||
def lchan_nr_from_cla(cla: int) -> int:
|
def lchan_nr_from_cla(cla: int) -> int:
|
||||||
"""Resolve the logical channel number from the CLA byte."""
|
"""Resolve the logical channel number from the CLA byte."""
|
||||||
@@ -33,10 +29,11 @@ def lchan_nr_from_cla(cla: int) -> int:
|
|||||||
if cla >> 4 in [0x0, 0xA, 0x8]:
|
if cla >> 4 in [0x0, 0xA, 0x8]:
|
||||||
# Table 10.3
|
# Table 10.3
|
||||||
return cla & 0x03
|
return cla & 0x03
|
||||||
if cla & 0xD0 in [0x40, 0xC0]:
|
elif cla & 0xD0 in [0x40, 0xC0]:
|
||||||
# Table 10.4a
|
# Table 10.4a
|
||||||
return 4 + (cla & 0x0F)
|
return 4 + (cla & 0x0F)
|
||||||
raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
|
else:
|
||||||
|
raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
|
||||||
|
|
||||||
class RuntimeState:
|
class RuntimeState:
|
||||||
"""Represent the runtime state of a session with a card."""
|
"""Represent the runtime state of a session with a card."""
|
||||||
@@ -47,30 +44,22 @@ class RuntimeState:
|
|||||||
card : pysim.cards.Card instance
|
card : pysim.cards.Card instance
|
||||||
profile : CardProfile instance
|
profile : CardProfile instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.mf = CardMF(profile=profile)
|
self.mf = CardMF(profile=profile)
|
||||||
self.card = card
|
self.card = card
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
self.lchan = {}
|
self.lchan = {}
|
||||||
# the basic logical channel always exists
|
# the basic logical channel always exists
|
||||||
self.lchan[0] = RuntimeLchan(0, self)
|
self.lchan[0] = RuntimeLchan(0, self)
|
||||||
# this is a dict of card identities which different parts of the code might populate,
|
|
||||||
# typically with something like ICCID, EID, ATR, ...
|
|
||||||
self.identity = {}
|
|
||||||
self.adm_verified = False
|
|
||||||
|
|
||||||
# make sure the class and selection control bytes, which are specified
|
# make sure the class and selection control bytes, which are specified
|
||||||
# by the card profile are used
|
# by the card profile are used
|
||||||
self.card.set_apdu_parameter(
|
self.card.set_apdu_parameter(
|
||||||
cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
|
cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
|
||||||
|
|
||||||
# make sure MF is selected before probing for Addons
|
|
||||||
self.lchan[0].select('MF')
|
|
||||||
|
|
||||||
for addon_cls in self.profile.addons:
|
for addon_cls in self.profile.addons:
|
||||||
addon = addon_cls()
|
addon = addon_cls()
|
||||||
if addon.probe(self.card):
|
if addon.probe(self.card):
|
||||||
log.info("Detected %s Add-on \"%s\"" % (self.profile, addon))
|
print("Detected %s Add-on \"%s\"" % (self.profile, addon))
|
||||||
for f in addon.files_in_mf:
|
for f in addon.files_in_mf:
|
||||||
self.mf.add_file(f)
|
self.mf.add_file(f)
|
||||||
|
|
||||||
@@ -104,18 +93,18 @@ class RuntimeState:
|
|||||||
apps_taken = []
|
apps_taken = []
|
||||||
if aids_card:
|
if aids_card:
|
||||||
aids_taken = []
|
aids_taken = []
|
||||||
log.info("AIDs on card:")
|
print("AIDs on card:")
|
||||||
for a in aids_card:
|
for a in aids_card:
|
||||||
for f in apps_profile:
|
for f in apps_profile:
|
||||||
if f.aid in a:
|
if f.aid in a:
|
||||||
log.info(" %s: %s (EF.DIR)" % (f.name, a))
|
print(" %s: %s (EF.DIR)" % (f.name, a))
|
||||||
aids_taken.append(a)
|
aids_taken.append(a)
|
||||||
apps_taken.append(f)
|
apps_taken.append(f)
|
||||||
aids_unknown = set(aids_card) - set(aids_taken)
|
aids_unknown = set(aids_card) - set(aids_taken)
|
||||||
for a in aids_unknown:
|
for a in aids_unknown:
|
||||||
log.info(" unknown: %s (EF.DIR)" % a)
|
print(" unknown: %s (EF.DIR)" % a)
|
||||||
else:
|
else:
|
||||||
log.warn("EF.DIR seems to be empty!")
|
print("warning: EF.DIR seems to be empty!")
|
||||||
|
|
||||||
# Some card applications may not be registered in EF.DIR, we will actively
|
# Some card applications may not be registered in EF.DIR, we will actively
|
||||||
# probe for those applications
|
# probe for those applications
|
||||||
@@ -127,10 +116,10 @@ class RuntimeState:
|
|||||||
# no problem when we access the card object directly without caring
|
# no problem when we access the card object directly without caring
|
||||||
# about updating other states. For normal selects at runtime, the
|
# about updating other states. For normal selects at runtime, the
|
||||||
# caller must use the lchan provided methods select or select_file!
|
# caller must use the lchan provided methods select or select_file!
|
||||||
_data, sw = self.card.select_adf_by_aid(f.aid)
|
data, sw = self.card.select_adf_by_aid(f.aid)
|
||||||
self.selected_adf = f
|
self.selected_adf = f
|
||||||
if sw == "9000":
|
if sw == "9000":
|
||||||
log.info(" %s: %s" % (f.name, f.aid))
|
print(" %s: %s" % (f.name, f.aid))
|
||||||
apps_taken.append(f)
|
apps_taken.append(f)
|
||||||
except (SwMatchError, ProtocolError):
|
except (SwMatchError, ProtocolError):
|
||||||
pass
|
pass
|
||||||
@@ -143,19 +132,13 @@ class RuntimeState:
|
|||||||
"""
|
"""
|
||||||
# delete all lchan != 0 (basic lchan)
|
# delete all lchan != 0 (basic lchan)
|
||||||
for lchan_nr in list(self.lchan.keys()):
|
for lchan_nr in list(self.lchan.keys()):
|
||||||
self.lchan[lchan_nr].scc.scp = None
|
|
||||||
if lchan_nr == 0:
|
if lchan_nr == 0:
|
||||||
continue
|
continue
|
||||||
del self.lchan[lchan_nr]
|
del self.lchan[lchan_nr]
|
||||||
self.adm_verified = False
|
atr = i2h(self.card.reset())
|
||||||
atr = self.card.reset()
|
|
||||||
if cmd_app:
|
|
||||||
cmd_app.lchan = self.lchan[0]
|
|
||||||
# select MF to reset internal state and to verify card really works
|
# select MF to reset internal state and to verify card really works
|
||||||
self.lchan[0].select('MF', cmd_app)
|
self.lchan[0].select('MF', cmd_app)
|
||||||
self.lchan[0].selected_adf = None
|
self.lchan[0].selected_adf = None
|
||||||
# store ATR as part of our card identities dict
|
|
||||||
self.identity['ATR'] = atr
|
|
||||||
return atr
|
return atr
|
||||||
|
|
||||||
def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
|
def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
|
||||||
@@ -220,18 +203,6 @@ class RuntimeLchan:
|
|||||||
def selected_file_num_of_rec(self) -> Optional[int]:
|
def selected_file_num_of_rec(self) -> Optional[int]:
|
||||||
return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
|
return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
|
||||||
|
|
||||||
def selected_file_record_len(self) -> Optional[int]:
|
|
||||||
return self.selected_file_fcp['file_descriptor'].get('record_len')
|
|
||||||
|
|
||||||
def selected_file_size(self) -> Optional[int]:
|
|
||||||
return self.selected_file_fcp.get('file_size')
|
|
||||||
|
|
||||||
def selected_file_reserved_file_size(self) -> Optional[int]:
|
|
||||||
return self.selected_file_fcp['proprietary_information'].get('reserved_file_size')
|
|
||||||
|
|
||||||
def selected_file_maximum_file_size(self) -> Optional[int]:
|
|
||||||
return self.selected_file_fcp['proprietary_information'].get('maximum_file_size')
|
|
||||||
|
|
||||||
def get_cwd(self) -> CardDF:
|
def get_cwd(self) -> CardDF:
|
||||||
"""Obtain the current working directory.
|
"""Obtain the current working directory.
|
||||||
|
|
||||||
@@ -256,42 +227,6 @@ class RuntimeLchan:
|
|||||||
node = node.parent
|
node = node.parent
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_file_by_name(self, name: str) -> CardFile:
|
|
||||||
"""Obtain the file object from the file system tree by its name without actually selecting the file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
CardFile() instance or None"""
|
|
||||||
|
|
||||||
# handling of entire paths with multiple directories/elements
|
|
||||||
if '/' in name:
|
|
||||||
pathlist = name.split('/')
|
|
||||||
# treat /DF.GSM/foo like MF/DF.GSM/foo
|
|
||||||
if pathlist[0] == '':
|
|
||||||
pathlist[0] = 'MF'
|
|
||||||
else:
|
|
||||||
pathlist = [name]
|
|
||||||
|
|
||||||
# start in the current working directory (we can still
|
|
||||||
# select any ADF and the MF from here, so those will be
|
|
||||||
# among the selectables).
|
|
||||||
file = self.get_cwd()
|
|
||||||
|
|
||||||
for p in pathlist:
|
|
||||||
# Look for the next file in the path list
|
|
||||||
selectables = file.get_selectables()
|
|
||||||
file = None
|
|
||||||
for selectable in selectables:
|
|
||||||
if selectable == p:
|
|
||||||
file = selectables[selectable]
|
|
||||||
break
|
|
||||||
|
|
||||||
# When we hit none, then the given path must be invalid
|
|
||||||
if file is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Return the file object found at the tip of the path
|
|
||||||
return file
|
|
||||||
|
|
||||||
def interpret_sw(self, sw: str):
|
def interpret_sw(self, sw: str):
|
||||||
"""Interpret a given status word relative to the currently selected application
|
"""Interpret a given status word relative to the currently selected application
|
||||||
or the underlying card profile.
|
or the underlying card profile.
|
||||||
@@ -320,30 +255,29 @@ class RuntimeLchan:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
|
"Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
|
||||||
|
|
||||||
# unregister commands of old file
|
self._select_pre(cmd_app)
|
||||||
self.unregister_cmds(cmd_app)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# We access the card through the select_file method of the scc object.
|
# We access the card through the select_file method of the scc object.
|
||||||
# If we succeed, we know that the file exists on the card and we may
|
# If we succeed, we know that the file exists on the card and we may
|
||||||
# proceed with creating a new CardEF object in the local file model at
|
# proceed with creating a new CardEF object in the local file model at
|
||||||
# run time. In case the file does not exist on the card, we just abort.
|
# run time. In case the file does not exist on the card, we just abort.
|
||||||
# The state on the card (selected file/application) won't be changed,
|
# The state on the card (selected file/application) wont't be changed,
|
||||||
# so we do not have to update any state in that case.
|
# so we do not have to update any state in that case.
|
||||||
(data, _sw) = self.scc.select_file(fid)
|
(data, sw) = self.scc.select_file(fid)
|
||||||
except SwMatchError as swm:
|
except SwMatchError as swm:
|
||||||
self._select_post(cmd_app)
|
self._select_post(cmd_app)
|
||||||
k = self.interpret_sw(swm.sw_actual)
|
k = self.interpret_sw(swm.sw_actual)
|
||||||
if not k:
|
if not k:
|
||||||
raise swm
|
raise(swm)
|
||||||
raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1])) from swm
|
raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
|
||||||
|
|
||||||
select_resp = self.selected_file.decode_select_response(data)
|
select_resp = self.selected_file.decode_select_response(data)
|
||||||
if select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df':
|
if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
|
||||||
f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
|
f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
|
||||||
desc="dedicated file, manually added at runtime")
|
desc="dedicated file, manually added at runtime")
|
||||||
else:
|
else:
|
||||||
if select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent':
|
if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
|
||||||
f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
|
f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
|
||||||
desc="elementary file, manually added at runtime")
|
desc="elementary file, manually added at runtime")
|
||||||
else:
|
else:
|
||||||
@@ -354,6 +288,12 @@ class RuntimeLchan:
|
|||||||
|
|
||||||
self._select_post(cmd_app, f, data)
|
self._select_post(cmd_app, f, data)
|
||||||
|
|
||||||
|
def _select_pre(self, cmd_app):
|
||||||
|
# unregister commands of old file
|
||||||
|
if cmd_app and self.selected_file.shell_commands:
|
||||||
|
for c in self.selected_file.shell_commands:
|
||||||
|
cmd_app.unregister_command_set(c)
|
||||||
|
|
||||||
def _select_post(self, cmd_app, file:Optional[CardFile] = None, select_resp_data = None):
|
def _select_post(self, cmd_app, file:Optional[CardFile] = None, select_resp_data = None):
|
||||||
# we store some reference data (see above) about the currently selected file.
|
# we store some reference data (see above) about the currently selected file.
|
||||||
# This data must be updated after every select.
|
# This data must be updated after every select.
|
||||||
@@ -364,12 +304,11 @@ class RuntimeLchan:
|
|||||||
if select_resp_data:
|
if select_resp_data:
|
||||||
self.selected_file_fcp_hex = select_resp_data
|
self.selected_file_fcp_hex = select_resp_data
|
||||||
self.selected_file_fcp = self.selected_file.decode_select_response(select_resp_data)
|
self.selected_file_fcp = self.selected_file.decode_select_response(select_resp_data)
|
||||||
else:
|
|
||||||
self.selected_file_fcp_hex = None
|
|
||||||
self.selected_file_fcp = None
|
|
||||||
|
|
||||||
# register commands of new file
|
# register commands of new file
|
||||||
self.register_cmds(cmd_app)
|
if cmd_app and self.selected_file.shell_commands:
|
||||||
|
for c in self.selected_file.shell_commands:
|
||||||
|
cmd_app.register_command_set(c)
|
||||||
|
|
||||||
def select_file(self, file: CardFile, cmd_app=None):
|
def select_file(self, file: CardFile, cmd_app=None):
|
||||||
"""Select a file (EF, DF, ADF, MF, ...).
|
"""Select a file (EF, DF, ADF, MF, ...).
|
||||||
@@ -378,31 +317,11 @@ class RuntimeLchan:
|
|||||||
file : CardFile [or derived class] instance
|
file : CardFile [or derived class] instance
|
||||||
cmd_app : Command Application State (for unregistering old file commands)
|
cmd_app : Command Application State (for unregistering old file commands)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not isinstance(file, CardADF) and self.selected_adf and self.selected_adf.has_fs == False:
|
|
||||||
# Not every application that may be present on a GlobalPlatform card will support the SELECT
|
|
||||||
# command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
|
|
||||||
# SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
|
|
||||||
# Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
|
|
||||||
# "select by name" method, which means we can only select an application and not a file.
|
|
||||||
# The consequence of this is that we may get trapped in an application that does not have
|
|
||||||
# ISIM/USIM like file system support and the only way to leave that application is to select
|
|
||||||
# an ISIM/USIM application in order to get the file system access back.
|
|
||||||
#
|
|
||||||
# To automate this escape-route we will first select an arbitrary ADF that has file system support first
|
|
||||||
# and then continue normally.
|
|
||||||
for selectable in self.rs.mf.get_selectables().items():
|
|
||||||
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
|
|
||||||
self.select(selectable[1].name, cmd_app)
|
|
||||||
break
|
|
||||||
|
|
||||||
# we need to find a path from our self.selected_file to the destination
|
# we need to find a path from our self.selected_file to the destination
|
||||||
inter_path = self.selected_file.build_select_path_to(file)
|
inter_path = self.selected_file.build_select_path_to(file)
|
||||||
if not inter_path:
|
if not inter_path:
|
||||||
raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
|
raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
|
||||||
|
self._select_pre(cmd_app)
|
||||||
# unregister commands of old file
|
|
||||||
self.unregister_cmds(cmd_app)
|
|
||||||
|
|
||||||
# be sure the variables that we pass to _select_post contain valid values.
|
# be sure the variables that we pass to _select_post contain valid values.
|
||||||
selected_file = self.selected_file
|
selected_file = self.selected_file
|
||||||
@@ -418,13 +337,13 @@ class RuntimeLchan:
|
|||||||
# card directly since this would lead into an incoherence of the
|
# card directly since this would lead into an incoherence of the
|
||||||
# card state and the state of the lchan.
|
# card state and the state of the lchan.
|
||||||
if isinstance(f, CardADF):
|
if isinstance(f, CardADF):
|
||||||
(data, _sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
|
(data, sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
|
||||||
else:
|
else:
|
||||||
(data, _sw) = self.scc.select_file(f.fid)
|
(data, sw) = self.scc.select_file(f.fid)
|
||||||
selected_file = f
|
selected_file = f
|
||||||
except SwMatchError as swm:
|
except SwMatchError as swm:
|
||||||
self._select_post(cmd_app, selected_file, data)
|
self._select_post(cmd_app, selected_file, data)
|
||||||
raise swm
|
raise(swm)
|
||||||
|
|
||||||
self._select_post(cmd_app, f, data)
|
self._select_post(cmd_app, f, data)
|
||||||
|
|
||||||
@@ -472,20 +391,15 @@ class RuntimeLchan:
|
|||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
"""Request STATUS (current selected file FCP) from card."""
|
"""Request STATUS (current selected file FCP) from card."""
|
||||||
(data, _sw) = self.scc.status()
|
(data, sw) = self.scc.status()
|
||||||
return self.selected_file.decode_select_response(data)
|
return self.selected_file.decode_select_response(data)
|
||||||
|
|
||||||
def get_file_for_filename(self, name: str):
|
def get_file_for_selectable(self, name: str):
|
||||||
"""Get the related CardFile object for a specified filename."""
|
|
||||||
if is_hex(name):
|
|
||||||
name = name.lower()
|
|
||||||
sels = self.selected_file.get_selectables()
|
sels = self.selected_file.get_selectables()
|
||||||
return sels[name]
|
return sels[name]
|
||||||
|
|
||||||
def activate_file(self, name: str):
|
def activate_file(self, name: str):
|
||||||
"""Request ACTIVATE FILE of specified file."""
|
"""Request ACTIVATE FILE of specified file."""
|
||||||
if is_hex(name):
|
|
||||||
name = name.lower()
|
|
||||||
sels = self.selected_file.get_selectables()
|
sels = self.selected_file.get_selectables()
|
||||||
f = sels[name]
|
f = sels[name]
|
||||||
data, sw = self.scc.activate_file(f.fid)
|
data, sw = self.scc.activate_file(f.fid)
|
||||||
@@ -501,8 +415,7 @@ class RuntimeLchan:
|
|||||||
binary data read from the file
|
binary data read from the file
|
||||||
"""
|
"""
|
||||||
if not isinstance(self.selected_file, TransparentEF):
|
if not isinstance(self.selected_file, TransparentEF):
|
||||||
raise TypeError("Only works with TransparentEF, but %s is %s" % (self.selected_file,
|
raise TypeError("Only works with TransparentEF")
|
||||||
self.selected_file.__class__.__mro__))
|
|
||||||
return self.scc.read_binary(self.selected_file.fid, length, offset)
|
return self.scc.read_binary(self.selected_file.fid, length, offset)
|
||||||
|
|
||||||
def read_binary_dec(self) -> Tuple[dict, str]:
|
def read_binary_dec(self) -> Tuple[dict, str]:
|
||||||
@@ -518,47 +431,6 @@ class RuntimeLchan:
|
|||||||
dec_data = self.selected_file.decode_hex(data)
|
dec_data = self.selected_file.decode_hex(data)
|
||||||
return (dec_data, sw)
|
return (dec_data, sw)
|
||||||
|
|
||||||
def __get_writeable_size(self):
|
|
||||||
""" Determine the writable size (file or record) using the cached FCP parameters of the currently selected
|
|
||||||
file. Return None in case the writeable size cannot be determined (no FCP available, FCP lacks size
|
|
||||||
information).
|
|
||||||
"""
|
|
||||||
fcp = self.selected_file_fcp
|
|
||||||
if not fcp:
|
|
||||||
return None
|
|
||||||
|
|
||||||
structure = fcp.get('file_descriptor', {}).get('file_descriptor_byte', {}).get('structure')
|
|
||||||
if not structure:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if structure == 'transparent':
|
|
||||||
return fcp.get('file_size')
|
|
||||||
elif structure == 'linear_fixed':
|
|
||||||
return fcp.get('file_descriptor', {}).get('record_len')
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __check_writeable_size(self, data_len):
|
|
||||||
""" Guard against unsuccessful writes caused by attempts to write data that exceeds the file limits. """
|
|
||||||
|
|
||||||
writeable_size = self.__get_writeable_size()
|
|
||||||
if not writeable_size:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(self.selected_file, TransparentEF):
|
|
||||||
writeable_name = "file"
|
|
||||||
elif isinstance(self.selected_file, LinFixedEF):
|
|
||||||
writeable_name = "record"
|
|
||||||
else:
|
|
||||||
writeable_name = "object"
|
|
||||||
|
|
||||||
if data_len > writeable_size:
|
|
||||||
raise TypeError("Data length (%u) exceeds %s size (%u) by %u bytes" %
|
|
||||||
(data_len, writeable_name, writeable_size, data_len - writeable_size))
|
|
||||||
elif data_len < writeable_size:
|
|
||||||
log.warn("Data length (%u) less than %s size (%u), leaving %u unwritten bytes at the end of the %s" %
|
|
||||||
(data_len, writeable_name, writeable_size, writeable_size - data_len, writeable_name))
|
|
||||||
|
|
||||||
def update_binary(self, data_hex: str, offset: int = 0):
|
def update_binary(self, data_hex: str, offset: int = 0):
|
||||||
"""Update transparent EF binary data.
|
"""Update transparent EF binary data.
|
||||||
|
|
||||||
@@ -567,9 +439,7 @@ class RuntimeLchan:
|
|||||||
offset : Offset into the file from which to write 'data_hex'
|
offset : Offset into the file from which to write 'data_hex'
|
||||||
"""
|
"""
|
||||||
if not isinstance(self.selected_file, TransparentEF):
|
if not isinstance(self.selected_file, TransparentEF):
|
||||||
raise TypeError("Only works with TransparentEF, but %s is %s" % (self.selected_file,
|
raise TypeError("Only works with TransparentEF")
|
||||||
self.selected_file.__class__.__mro__))
|
|
||||||
self.__check_writeable_size(len(data_hex) // 2 + offset)
|
|
||||||
return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
|
return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
|
||||||
|
|
||||||
def update_binary_dec(self, data: dict):
|
def update_binary_dec(self, data: dict):
|
||||||
@@ -579,7 +449,7 @@ class RuntimeLchan:
|
|||||||
Args:
|
Args:
|
||||||
data : abstract data which is to be encoded and written
|
data : abstract data which is to be encoded and written
|
||||||
"""
|
"""
|
||||||
data_hex = self.selected_file.encode_hex(data, self.selected_file_size())
|
data_hex = self.selected_file.encode_hex(data)
|
||||||
return self.update_binary(data_hex)
|
return self.update_binary(data_hex)
|
||||||
|
|
||||||
def read_record(self, rec_nr: int = 0):
|
def read_record(self, rec_nr: int = 0):
|
||||||
@@ -591,8 +461,7 @@ class RuntimeLchan:
|
|||||||
hex string of binary data contained in record
|
hex string of binary data contained in record
|
||||||
"""
|
"""
|
||||||
if not isinstance(self.selected_file, LinFixedEF):
|
if not isinstance(self.selected_file, LinFixedEF):
|
||||||
raise TypeError("Only works with Linear Fixed EF, but %s is %s" % (self.selected_file,
|
raise TypeError("Only works with Linear Fixed EF")
|
||||||
self.selected_file.__class__.__mro__))
|
|
||||||
# returns a string of hex nibbles
|
# returns a string of hex nibbles
|
||||||
return self.scc.read_record(self.selected_file.fid, rec_nr)
|
return self.scc.read_record(self.selected_file.fid, rec_nr)
|
||||||
|
|
||||||
@@ -615,9 +484,7 @@ class RuntimeLchan:
|
|||||||
data_hex : Hex string binary data to be written
|
data_hex : Hex string binary data to be written
|
||||||
"""
|
"""
|
||||||
if not isinstance(self.selected_file, LinFixedEF):
|
if not isinstance(self.selected_file, LinFixedEF):
|
||||||
raise TypeError("Only works with Linear Fixed EF, but %s is %s" % (self.selected_file,
|
raise TypeError("Only works with Linear Fixed EF")
|
||||||
self.selected_file.__class__.__mro__))
|
|
||||||
self.__check_writeable_size(len(data_hex) // 2)
|
|
||||||
return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
|
return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
|
||||||
conserve=self.rs.conserve_write,
|
conserve=self.rs.conserve_write,
|
||||||
leftpad=self.selected_file.leftpad)
|
leftpad=self.selected_file.leftpad)
|
||||||
@@ -630,7 +497,7 @@ class RuntimeLchan:
|
|||||||
rec_nr : Record number to read
|
rec_nr : Record number to read
|
||||||
data_hex : Abstract data to be written
|
data_hex : Abstract data to be written
|
||||||
"""
|
"""
|
||||||
data_hex = self.selected_file.encode_record_hex(data, rec_nr, self.selected_file_record_len())
|
data_hex = self.selected_file.encode_record_hex(data, rec_nr)
|
||||||
return self.update_record(rec_nr, data_hex)
|
return self.update_record(rec_nr, data_hex)
|
||||||
|
|
||||||
def retrieve_data(self, tag: int = 0):
|
def retrieve_data(self, tag: int = 0):
|
||||||
@@ -653,10 +520,9 @@ class RuntimeLchan:
|
|||||||
list of integer tags contained in EF
|
list of integer tags contained in EF
|
||||||
"""
|
"""
|
||||||
if not isinstance(self.selected_file, BerTlvEF):
|
if not isinstance(self.selected_file, BerTlvEF):
|
||||||
raise TypeError("Only works with BER-TLV EF, but %s is %s" % (self.selected_file,
|
raise TypeError("Only works with BER-TLV EF")
|
||||||
self.selected_file.__class__.__mro__))
|
data, sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
|
||||||
data, _sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
|
tag, length, value, remainder = bertlv_parse_one(h2b(data))
|
||||||
_tag, _length, value, _remainder = bertlv_parse_one(h2b(data))
|
|
||||||
return list(value)
|
return list(value)
|
||||||
|
|
||||||
def set_data(self, tag: int, data_hex: str):
|
def set_data(self, tag: int, data_hex: str):
|
||||||
@@ -667,18 +533,14 @@ class RuntimeLchan:
|
|||||||
data_hex : Hex string binary data to be written (value portion)
|
data_hex : Hex string binary data to be written (value portion)
|
||||||
"""
|
"""
|
||||||
if not isinstance(self.selected_file, BerTlvEF):
|
if not isinstance(self.selected_file, BerTlvEF):
|
||||||
raise TypeError("Only works with BER-TLV EF, but %s is %s" % (self.selected_file,
|
raise TypeError("Only works with BER-TLV EF")
|
||||||
self.selected_file.__class__.__mro__))
|
|
||||||
return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
|
return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
|
||||||
|
|
||||||
def register_cmds(self, cmd_app=None):
|
|
||||||
"""Register command set that is associated with the currently selected file"""
|
|
||||||
if cmd_app and self.selected_file.shell_commands:
|
|
||||||
for c in self.selected_file.shell_commands:
|
|
||||||
cmd_app.register_command_set(c)
|
|
||||||
|
|
||||||
def unregister_cmds(self, cmd_app=None):
|
def unregister_cmds(self, cmd_app=None):
|
||||||
"""Unregister command set that is associated with the currently selected file"""
|
"""Unregister all file specific commands."""
|
||||||
if cmd_app and self.selected_file.shell_commands:
|
if cmd_app and self.selected_file.shell_commands:
|
||||||
for c in self.selected_file.shell_commands:
|
for c in self.selected_file.shell_commands:
|
||||||
cmd_app.unregister_command_set(c)
|
cmd_app.unregister_command_set(c)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
# Generic code related to Secure Channel processing
|
|
||||||
#
|
|
||||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import abc
|
|
||||||
from osmocom.utils import b2h, h2b, Hexstr
|
|
||||||
|
|
||||||
from pySim.utils import ResTuple
|
|
||||||
|
|
||||||
class SecureChannel(abc.ABC):
|
|
||||||
@abc.abstractmethod
|
|
||||||
def wrap_cmd_apdu(self, apdu: bytes) -> bytes:
|
|
||||||
"""Wrap Command APDU according to specific Secure Channel Protocol."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
|
|
||||||
"""UnWrap Response-APDU according to specific Secure Channel Protocol."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def send_apdu_wrapper(self, send_fn: callable, pdu: Hexstr, *args, **kwargs) -> ResTuple:
|
|
||||||
"""Wrapper function to wrap command APDU and unwrap response APDU around send_apdu callable."""
|
|
||||||
pdu_wrapped = b2h(self.wrap_cmd_apdu(h2b(pdu)))
|
|
||||||
res, sw = send_fn(pdu_wrapped, *args, **kwargs)
|
|
||||||
res_unwrapped = b2h(self.unwrap_rsp_apdu(h2b(sw), h2b(res)))
|
|
||||||
return res_unwrapped, sw
|
|
||||||
107
pySim/sms.py
107
pySim/sms.py
@@ -20,11 +20,12 @@
|
|||||||
import typing
|
import typing
|
||||||
import abc
|
import abc
|
||||||
from bidict import bidict
|
from bidict import bidict
|
||||||
from construct import Int8ub, Byte, Bit, Flag, BitsInteger
|
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger, Flag
|
||||||
from construct import Struct, Enum, Tell, BitStruct, this, Padding
|
from construct import Struct, Enum, Tell, BitStruct, this, Padding
|
||||||
from construct import Prefixed, GreedyRange
|
from construct import Prefixed, GreedyRange, GreedyBytes
|
||||||
from osmocom.construct import BcdAdapter, TonNpi, Bytes, GreedyBytes
|
|
||||||
from osmocom.utils import Hexstr, h2b, b2h
|
from pySim.construct import HexAdapter, BcdAdapter, TonNpi
|
||||||
|
from pySim.utils import Hexstr, h2b, b2h
|
||||||
|
|
||||||
from smpp.pdu import pdu_types, operations
|
from smpp.pdu import pdu_types, operations
|
||||||
|
|
||||||
@@ -50,13 +51,13 @@ class UserDataHeader:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
|
def fromBytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
|
||||||
if isinstance(inb, str):
|
if isinstance(inb, str):
|
||||||
inb = h2b(inb)
|
inb = h2b(inb)
|
||||||
res = cls._construct.parse(inb)
|
res = cls._construct.parse(inb)
|
||||||
return cls(res['ies']), res['data']
|
return cls(res['ies']), res['data']
|
||||||
|
|
||||||
def to_bytes(self) -> bytes:
|
def toBytes(self) -> bytes:
|
||||||
return self._construct.build({'ies':self.ies, 'data':b''})
|
return self._construct.build({'ies':self.ies, 'data':b''})
|
||||||
|
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ class AddressField:
|
|||||||
return 'AddressField(TON=%s, NPI=%s, %s)' % (self.ton, self.npi, self.digits)
|
return 'AddressField(TON=%s, NPI=%s, %s)' % (self.ton, self.npi, self.digits)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
|
def fromBytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
|
||||||
"""Construct an AddressField instance from the binary T-PDU address format."""
|
"""Construct an AddressField instance from the binary T-PDU address format."""
|
||||||
if isinstance(inb, str):
|
if isinstance(inb, str):
|
||||||
inb = h2b(inb)
|
inb = h2b(inb)
|
||||||
@@ -128,16 +129,16 @@ class AddressField:
|
|||||||
return cls(res['digits'][:res['addr_len']], ton, npi), inb[res['tell']:]
|
return cls(res['digits'][:res['addr_len']], ton, npi), inb[res['tell']:]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_smpp(cls, addr, ton, npi) -> 'AddressField':
|
def fromSmpp(cls, addr, ton, npi) -> 'AddressField':
|
||||||
"""Construct an AddressField from {source,dest}_addr_{,ton,npi} attributes of smpp.pdu."""
|
"""Construct an AddressField from {source,dest}_addr_{,ton,npi} attributes of smpp.pdu."""
|
||||||
# return the resulting instance
|
# return the resulting instance
|
||||||
return cls(addr.decode('ascii'), AddressField.smpp_map_ton[ton.name], AddressField.smpp_map_npi[npi.name])
|
return cls(addr.decode('ascii'), AddressField.smpp_map_ton[ton.name], AddressField.smpp_map_npi[npi.name])
|
||||||
|
|
||||||
def to_smpp(self):
|
def toSmpp(self):
|
||||||
"""Return smpp.pdo.*.source,dest}_addr_{,ton,npi} attributes for given AddressField."""
|
"""Return smpp.pdo.*.source,dest}_addr_{,ton,npi} attributes for given AddressField."""
|
||||||
return (self.digits, self.smpp_map_ton.inverse[self.ton], self.smpp_map_npi.inverse[self.npi])
|
return (self.digits, self.smpp_map_ton.inverse[self.ton], self.smpp_map_npi.inverse[self.npi])
|
||||||
|
|
||||||
def to_bytes(self) -> bytes:
|
def toBytes(self) -> bytes:
|
||||||
"""Encode the AddressField into the binary representation as used in T-PDU."""
|
"""Encode the AddressField into the binary representation as used in T-PDU."""
|
||||||
num_digits = len(self.digits)
|
num_digits = len(self.digits)
|
||||||
if num_digits % 2:
|
if num_digits % 2:
|
||||||
@@ -184,12 +185,13 @@ class SMS_DELIVER(SMS_TPDU):
|
|||||||
return '%s(MTI=%s, MMS=%s, LP=%s, RP=%s, UDHI=%s, SRI=%s, OA=%s, PID=%2x, DCS=%x, SCTS=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_mms, self.tp_lp, self.tp_rp, self.tp_udhi, self.tp_sri, self.tp_oa, self.tp_pid, self.tp_dcs, self.tp_scts, self.tp_udl, self.tp_ud)
|
return '%s(MTI=%s, MMS=%s, LP=%s, RP=%s, UDHI=%s, SRI=%s, OA=%s, PID=%2x, DCS=%x, SCTS=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_mms, self.tp_lp, self.tp_rp, self.tp_udhi, self.tp_sri, self.tp_oa, self.tp_pid, self.tp_dcs, self.tp_scts, self.tp_udl, self.tp_ud)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
|
def fromBytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
|
||||||
"""Construct a SMS_DELIVER instance from the binary encoded format as used in T-PDU."""
|
"""Construct a SMS_DELIVER instance from the binary encoded format as used in T-PDU."""
|
||||||
if isinstance(inb, str):
|
if isinstance(inb, str):
|
||||||
inb = h2b(inb)
|
inb = h2b(inb)
|
||||||
|
flags = inb[0]
|
||||||
d = SMS_DELIVER.flags_construct.parse(inb)
|
d = SMS_DELIVER.flags_construct.parse(inb)
|
||||||
oa, remainder = AddressField.from_bytes(inb[1:])
|
oa, remainder = AddressField.fromBytes(inb[1:])
|
||||||
d['tp_oa'] = oa
|
d['tp_oa'] = oa
|
||||||
offset = 0
|
offset = 0
|
||||||
d['tp_pid'] = remainder[offset]
|
d['tp_pid'] = remainder[offset]
|
||||||
@@ -204,7 +206,7 @@ class SMS_DELIVER(SMS_TPDU):
|
|||||||
d['tp_ud'] = remainder[offset:]
|
d['tp_ud'] = remainder[offset:]
|
||||||
return cls(**d)
|
return cls(**d)
|
||||||
|
|
||||||
def to_bytes(self) -> bytes:
|
def toBytes(self) -> bytes:
|
||||||
"""Encode a SMS_DELIVER instance to the binary encoded format as used in T-PDU."""
|
"""Encode a SMS_DELIVER instance to the binary encoded format as used in T-PDU."""
|
||||||
outb = bytearray()
|
outb = bytearray()
|
||||||
d = {
|
d = {
|
||||||
@@ -213,7 +215,7 @@ class SMS_DELIVER(SMS_TPDU):
|
|||||||
}
|
}
|
||||||
flags = SMS_DELIVER.flags_construct.build(d)
|
flags = SMS_DELIVER.flags_construct.build(d)
|
||||||
outb.extend(flags)
|
outb.extend(flags)
|
||||||
outb.extend(self.tp_oa.to_bytes())
|
outb.extend(self.tp_oa.toBytes())
|
||||||
outb.append(self.tp_pid)
|
outb.append(self.tp_pid)
|
||||||
outb.append(self.tp_dcs)
|
outb.append(self.tp_dcs)
|
||||||
outb.extend(self.tp_scts)
|
outb.extend(self.tp_scts)
|
||||||
@@ -223,18 +225,18 @@ class SMS_DELIVER(SMS_TPDU):
|
|||||||
return outb
|
return outb
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_smpp(cls, smpp_pdu) -> 'SMS_DELIVER':
|
def fromSmpp(cls, smpp_pdu) -> 'SMS_DELIVER':
|
||||||
"""Construct a SMS_DELIVER instance from the deliver format used by smpp.pdu."""
|
"""Construct a SMS_DELIVER instance from the deliver format used by smpp.pdu."""
|
||||||
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
|
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
|
||||||
return cls.from_smpp_submit(smpp_pdu)
|
return cls.fromSmppSubmit(smpp_pdu)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_DELIVER':
|
def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_DELIVER':
|
||||||
"""Construct a SMS_DELIVER instance from the submit format used by smpp.pdu."""
|
"""Construct a SMS_DELIVER instance from the submit format used by smpp.pdu."""
|
||||||
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
|
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
|
||||||
tp_oa = AddressField.from_smpp(smpp_pdu.params['source_addr'],
|
tp_oa = AddressField.fromSmpp(smpp_pdu.params['source_addr'],
|
||||||
smpp_pdu.params['source_addr_ton'],
|
smpp_pdu.params['source_addr_ton'],
|
||||||
smpp_pdu.params['source_addr_npi'])
|
smpp_pdu.params['source_addr_npi'])
|
||||||
tp_ud = smpp_pdu.params['short_message']
|
tp_ud = smpp_pdu.params['short_message']
|
||||||
@@ -253,49 +255,6 @@ class SMS_DELIVER(SMS_TPDU):
|
|||||||
}
|
}
|
||||||
return cls(**d)
|
return cls(**d)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_submit(cls, submit: 'SMS_SUBMIT') -> 'SMS_DELIVER':
|
|
||||||
"""Construct a SMS_DELIVER instance from a SMS_SUBMIT instance."""
|
|
||||||
d = {
|
|
||||||
# common fields (SMS_TPDU base class) which exist in submit, so we can copy them
|
|
||||||
'tp_mti': submit.tp_mti,
|
|
||||||
'tp_rp': submit.tp_rp,
|
|
||||||
'tp_udhi': submit.tp_udhi,
|
|
||||||
'tp_pid': submit.tp_pid,
|
|
||||||
'tp_dcs': submit.tp_dcs,
|
|
||||||
'tp_udl': submit.tp_udl,
|
|
||||||
'tp_ud': submit.tp_ud,
|
|
||||||
# SMS_DELIVER specific fields
|
|
||||||
'tp_lp': False,
|
|
||||||
'tp_mms': False,
|
|
||||||
'tp_oa': None,
|
|
||||||
'tp_scts': h2b('22705200000000'), # FIXME
|
|
||||||
'tp_sri': False,
|
|
||||||
}
|
|
||||||
return cls(**d)
|
|
||||||
|
|
||||||
def to_smpp(self) -> pdu_types.PDU:
|
|
||||||
"""Translate a SMS_DELIVER instance to a smpp.pdu.operations.DeliverSM instance."""
|
|
||||||
# we only deal with binary SMS here:
|
|
||||||
if self.tp_dcs != 0xF6:
|
|
||||||
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
|
|
||||||
dcs = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
|
||||||
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
|
|
||||||
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT,
|
|
||||||
gsmFeatures=[pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET])
|
|
||||||
if self.tp_oa:
|
|
||||||
oa_digits, oa_ton, oa_npi = self.tp_oa.to_smpp()
|
|
||||||
else:
|
|
||||||
oa_digits, oa_ton, oa_npi = None, None, None
|
|
||||||
return operations.DeliverSM(source_addr=oa_digits,
|
|
||||||
source_addr_ton=oa_ton,
|
|
||||||
source_addr_npi=oa_npi,
|
|
||||||
#destination_addr=ESME_MSISDN,
|
|
||||||
esm_class=esm_class,
|
|
||||||
protocol_id=self.tp_pid,
|
|
||||||
data_coding=dcs,
|
|
||||||
short_message=self.tp_ud)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SMS_SUBMIT(SMS_TPDU):
|
class SMS_SUBMIT(SMS_TPDU):
|
||||||
@@ -317,7 +276,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
|||||||
return '%s(MTI=%s, RD=%s, VPF=%u, RP=%s, UDHI=%s, SRR=%s, DA=%s, PID=%2x, DCS=%x, VP=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_rd, self.tp_vpf, self.tp_rp, self.tp_udhi, self.tp_srr, self.tp_da, self.tp_pid, self.tp_dcs, self.tp_vp, self.tp_udl, self.tp_ud)
|
return '%s(MTI=%s, RD=%s, VPF=%u, RP=%s, UDHI=%s, SRR=%s, DA=%s, PID=%2x, DCS=%x, VP=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_rd, self.tp_vpf, self.tp_rp, self.tp_udhi, self.tp_srr, self.tp_da, self.tp_pid, self.tp_dcs, self.tp_vp, self.tp_udl, self.tp_ud)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
|
def fromBytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
|
||||||
"""Construct a SMS_SUBMIT instance from the binary encoded format as used in T-PDU."""
|
"""Construct a SMS_SUBMIT instance from the binary encoded format as used in T-PDU."""
|
||||||
offset = 0
|
offset = 0
|
||||||
if isinstance(inb, str):
|
if isinstance(inb, str):
|
||||||
@@ -326,7 +285,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
|||||||
offset += 1
|
offset += 1
|
||||||
d['tp_mr']= inb[offset]
|
d['tp_mr']= inb[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
da, remainder = AddressField.from_bytes(inb[2:])
|
da, remainder = AddressField.fromBytes(inb[2:])
|
||||||
d['tp_da'] = da
|
d['tp_da'] = da
|
||||||
|
|
||||||
offset = 0
|
offset = 0
|
||||||
@@ -344,10 +303,12 @@ class SMS_SUBMIT(SMS_TPDU):
|
|||||||
# TODO: further decode
|
# TODO: further decode
|
||||||
d['tp_vp'] = remainder[offset:offset+7]
|
d['tp_vp'] = remainder[offset:offset+7]
|
||||||
offset += 7
|
offset += 7
|
||||||
|
pass
|
||||||
elif d['tp_vpf'] == 'absolute':
|
elif d['tp_vpf'] == 'absolute':
|
||||||
# TODO: further decode
|
# TODO: further decode
|
||||||
d['tp_vp'] = remainder[offset:offset+7]
|
d['tp_vp'] = remainder[offset:offset+7]
|
||||||
offset += 7
|
offset += 7
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValueError('Invalid VPF: %s' % d['tp_vpf'])
|
raise ValueError('Invalid VPF: %s' % d['tp_vpf'])
|
||||||
d['tp_udl'] = remainder[offset]
|
d['tp_udl'] = remainder[offset]
|
||||||
@@ -355,7 +316,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
|||||||
d['tp_ud'] = remainder[offset:]
|
d['tp_ud'] = remainder[offset:]
|
||||||
return cls(**d)
|
return cls(**d)
|
||||||
|
|
||||||
def to_bytes(self) -> bytes:
|
def toBytes(self) -> bytes:
|
||||||
"""Encode a SMS_SUBMIT instance to the binary encoded format as used in T-PDU."""
|
"""Encode a SMS_SUBMIT instance to the binary encoded format as used in T-PDU."""
|
||||||
outb = bytearray()
|
outb = bytearray()
|
||||||
d = {
|
d = {
|
||||||
@@ -365,7 +326,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
|||||||
flags = SMS_SUBMIT.flags_construct.build(d)
|
flags = SMS_SUBMIT.flags_construct.build(d)
|
||||||
outb.extend(flags)
|
outb.extend(flags)
|
||||||
outb.append(self.tp_mr)
|
outb.append(self.tp_mr)
|
||||||
outb.extend(self.tp_da.to_bytes())
|
outb.extend(self.tp_da.toBytes())
|
||||||
outb.append(self.tp_pid)
|
outb.append(self.tp_pid)
|
||||||
outb.append(self.tp_dcs)
|
outb.append(self.tp_dcs)
|
||||||
if self.tp_vpf != 'none':
|
if self.tp_vpf != 'none':
|
||||||
@@ -375,20 +336,20 @@ class SMS_SUBMIT(SMS_TPDU):
|
|||||||
return outb
|
return outb
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_smpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
def fromSmpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
||||||
"""Construct a SMS_SUBMIT instance from the format used by smpp.pdu."""
|
"""Construct a SMS_SUBMIT instance from the format used by smpp.pdu."""
|
||||||
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
|
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
|
||||||
return cls.from_smpp_submit(smpp_pdu)
|
return cls.fromSmppSubmit(smpp_pdu)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
def fromSmppSubmit(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
||||||
"""Construct a SMS_SUBMIT instance from the submit format used by smpp.pdu."""
|
"""Construct a SMS_SUBMIT instance from the submit format used by smpp.pdu."""
|
||||||
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
|
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
|
||||||
tp_da = AddressField.from_smpp(smpp_pdu.params['destination_addr'],
|
tp_da = AddressField.fromSmpp(smpp_pdu.params['destination_addr'],
|
||||||
smpp_pdu.params['dest_addr_ton'],
|
smpp_pdu.params['dest_addr_ton'],
|
||||||
smpp_pdu.params['dest_addr_npi'])
|
smpp_pdu.params['dest_addr_npi'])
|
||||||
tp_ud = smpp_pdu.params['short_message']
|
tp_ud = smpp_pdu.params['short_message']
|
||||||
#vp_smpp = smpp_pdu.params['validity_period']
|
#vp_smpp = smpp_pdu.params['validity_period']
|
||||||
#if not vp_smpp:
|
#if not vp_smpp:
|
||||||
@@ -409,7 +370,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
|||||||
}
|
}
|
||||||
return cls(**d)
|
return cls(**d)
|
||||||
|
|
||||||
def to_smpp(self) -> pdu_types.PDU:
|
def toSmpp(self) -> pdu_types.PDU:
|
||||||
"""Translate a SMS_SUBMIT instance to a smpp.pdu.operations.SubmitSM instance."""
|
"""Translate a SMS_SUBMIT instance to a smpp.pdu.operations.SubmitSM instance."""
|
||||||
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT)
|
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT)
|
||||||
reg_del = pdu_types.RegisteredDelivery(pdu_types.RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED)
|
reg_del = pdu_types.RegisteredDelivery(pdu_types.RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED)
|
||||||
@@ -421,7 +382,7 @@ class SMS_SUBMIT(SMS_TPDU):
|
|||||||
if self.tp_dcs != 0xF6:
|
if self.tp_dcs != 0xF6:
|
||||||
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
|
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
|
||||||
dc = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT, pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
|
dc = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT, pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
|
||||||
(daddr, ton, npi) = self.tp_da.to_smpp()
|
(daddr, ton, npi) = self.tp_da.toSmpp()
|
||||||
return operations.SubmitSM(service_type='',
|
return operations.SubmitSM(service_type='',
|
||||||
source_addr_ton=pdu_types.AddrTon.ALPHANUMERIC,
|
source_addr_ton=pdu_types.AddrTon.ALPHANUMERIC,
|
||||||
source_addr_npi=pdu_types.AddrNpi.UNKNOWN,
|
source_addr_npi=pdu_types.AddrNpi.UNKNOWN,
|
||||||
|
|||||||
@@ -17,15 +17,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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from struct import unpack
|
from pytlv.TLV import *
|
||||||
from construct import FlagsEnum, Byte, Struct, Int8ub, Mapping, Enum, Padding, BitsInteger
|
from struct import pack, unpack
|
||||||
from construct import Bit, this, Int32ub, Int16ub, Nibble, BytesInteger, GreedyRange, Const
|
from pySim.utils import *
|
||||||
from construct import Optional as COptional
|
|
||||||
from osmocom.utils import *
|
|
||||||
from osmocom.construct import *
|
|
||||||
|
|
||||||
from pySim.filesystem import *
|
from pySim.filesystem import *
|
||||||
from pySim.runtime import RuntimeState
|
from pySim.runtime import RuntimeState
|
||||||
|
from pySim.ts_102_221 import CardProfileUICC
|
||||||
|
from pySim.construct import *
|
||||||
|
from construct import *
|
||||||
import pySim
|
import pySim
|
||||||
|
|
||||||
key_type2str = {
|
key_type2str = {
|
||||||
@@ -51,13 +50,13 @@ class EF_PIN(TransparentEF):
|
|||||||
( 'f1030331323334ffffffff0a0a3132333435363738',
|
( 'f1030331323334ffffffff0a0a3132333435363738',
|
||||||
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
|
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
|
||||||
'not_initialized': False, 'disabled': True },
|
'not_initialized': False, 'disabled': True },
|
||||||
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': b'1234',
|
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '31323334',
|
||||||
'puk': { 'attempts_remaining': 10, 'maximum_attempts': 10, 'puk': b'12345678' }
|
'puk': { 'attempts_remaining': 10, 'maximum_attempts': 10, 'puk': '3132333435363738' }
|
||||||
} ),
|
} ),
|
||||||
( 'f003039999999999999999',
|
( 'f003039999999999999999',
|
||||||
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
|
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
|
||||||
'not_initialized': False, 'disabled': False },
|
'not_initialized': False, 'disabled': False },
|
||||||
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': h2b('9999999999999999'),
|
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '9999999999999999',
|
||||||
'puk': None } ),
|
'puk': None } ),
|
||||||
]
|
]
|
||||||
def __init__(self, fid='6f01', name='EF.CHV1'):
|
def __init__(self, fid='6f01', name='EF.CHV1'):
|
||||||
@@ -66,32 +65,29 @@ class EF_PIN(TransparentEF):
|
|||||||
change_able=0x40, valid=0x80)
|
change_able=0x40, valid=0x80)
|
||||||
PukStruct = Struct('attempts_remaining'/Int8ub,
|
PukStruct = Struct('attempts_remaining'/Int8ub,
|
||||||
'maximum_attempts'/Int8ub,
|
'maximum_attempts'/Int8ub,
|
||||||
'puk'/Rpad(Bytes(8)))
|
'puk'/HexAdapter(Rpad(Bytes(8))))
|
||||||
self._construct = Struct('state'/StateByte,
|
self._construct = Struct('state'/StateByte,
|
||||||
'attempts_remaining'/Int8ub,
|
'attempts_remaining'/Int8ub,
|
||||||
'maximum_attempts'/Int8ub,
|
'maximum_attempts'/Int8ub,
|
||||||
'pin'/Rpad(Bytes(8)),
|
'pin'/HexAdapter(Rpad(Bytes(8))),
|
||||||
'puk'/COptional(PukStruct))
|
'puk'/Optional(PukStruct))
|
||||||
|
|
||||||
|
|
||||||
class EF_MILENAGE_CFG(TransparentEF):
|
class EF_MILENAGE_CFG(TransparentEF):
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( '40002040600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000400000000000000000000000000000008',
|
( '40002040600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000400000000000000000000000000000008',
|
||||||
{"r1": 64, "r2": 0, "r3": 32, "r4": 64, "r5": 96,
|
{"r1": 64, "r2": 0, "r3": 32, "r4": 64, "r5": 96, "c1": "00000000000000000000000000000000", "c2":
|
||||||
"c1": h2b("00000000000000000000000000000000"),
|
"00000000000000000000000000000001", "c3": "00000000000000000000000000000002", "c4":
|
||||||
"c2": h2b("00000000000000000000000000000001"),
|
"00000000000000000000000000000004", "c5": "00000000000000000000000000000008"} ),
|
||||||
"c3": h2b("00000000000000000000000000000002"),
|
|
||||||
"c4": h2b("00000000000000000000000000000004"),
|
|
||||||
"c5": h2b("00000000000000000000000000000008")} ),
|
|
||||||
]
|
]
|
||||||
def __init__(self, fid='6f21', name='EF.MILENAGE_CFG', desc='Milenage connfiguration'):
|
def __init__(self, fid='6f21', name='EF.MILENAGE_CFG', desc='Milenage connfiguration'):
|
||||||
super().__init__(fid, name=name, desc=desc)
|
super().__init__(fid, name=name, desc=desc)
|
||||||
self._construct = Struct('r1'/Int8ub, 'r2'/Int8ub, 'r3'/Int8ub, 'r4'/Int8ub, 'r5'/Int8ub,
|
self._construct = Struct('r1'/Int8ub, 'r2'/Int8ub, 'r3'/Int8ub, 'r4'/Int8ub, 'r5'/Int8ub,
|
||||||
'c1'/Bytes(16),
|
'c1'/HexAdapter(Bytes(16)),
|
||||||
'c2'/Bytes(16),
|
'c2'/HexAdapter(Bytes(16)),
|
||||||
'c3'/Bytes(16),
|
'c3'/HexAdapter(Bytes(16)),
|
||||||
'c4'/Bytes(16),
|
'c4'/HexAdapter(Bytes(16)),
|
||||||
'c5'/Bytes(16))
|
'c5'/HexAdapter(Bytes(16)))
|
||||||
|
|
||||||
|
|
||||||
class EF_0348_KEY(LinFixedEF):
|
class EF_0348_KEY(LinFixedEF):
|
||||||
@@ -105,18 +101,18 @@ class EF_0348_KEY(LinFixedEF):
|
|||||||
self._construct = Struct('security_domain'/Int8ub,
|
self._construct = Struct('security_domain'/Int8ub,
|
||||||
'key_set_version'/Int8ub,
|
'key_set_version'/Int8ub,
|
||||||
'key_len_and_type'/KeyLenAndType,
|
'key_len_and_type'/KeyLenAndType,
|
||||||
'key'/Bytes(this.key_len_and_type.key_length))
|
'key'/HexAdapter(Bytes(this.key_len_and_type.key_length)))
|
||||||
|
|
||||||
|
|
||||||
class EF_0348_COUNT(LinFixedEF):
|
class EF_0348_COUNT(LinFixedEF):
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( 'fe010000000000', {"sec_domain": 254, "key_set_version": 1, "counter": h2b("0000000000")} ),
|
( 'fe010000000000', {"sec_domain": 254, "key_set_version": 1, "counter": "0000000000"} ),
|
||||||
]
|
]
|
||||||
def __init__(self, fid='6f23', name='EF.0348_COUNT', desc='TS 03.48 OTA Counters'):
|
def __init__(self, fid='6f23', name='EF.0348_COUNT', desc='TS 03.48 OTA Counters'):
|
||||||
super().__init__(fid, name=name, desc=desc, rec_len=(7, 7))
|
super().__init__(fid, name=name, desc=desc, rec_len=(7, 7))
|
||||||
self._construct = Struct('sec_domain'/Int8ub,
|
self._construct = Struct('sec_domain'/Int8ub,
|
||||||
'key_set_version'/Int8ub,
|
'key_set_version'/Int8ub,
|
||||||
'counter'/Bytes(5))
|
'counter'/HexAdapter(Bytes(5)))
|
||||||
|
|
||||||
|
|
||||||
class EF_SIM_AUTH_COUNTER(TransparentEF):
|
class EF_SIM_AUTH_COUNTER(TransparentEF):
|
||||||
@@ -148,9 +144,8 @@ class EF_GP_DIV_DATA(LinFixedEF):
|
|||||||
class EF_SIM_AUTH_KEY(TransparentEF):
|
class EF_SIM_AUTH_KEY(TransparentEF):
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
||||||
{"cfg": {"sres_deriv_func": 1, "use_opc_instead_of_op": True, "algorithm": "milenage"},
|
{"cfg": {"sres_deriv_func": 1, "use_opc_instead_of_op": True, "algorithm": "milenage"}, "key":
|
||||||
"key": h2b("000102030405060708090a0b0c0d0e0f"),
|
"000102030405060708090a0b0c0d0e0f", "op_opc": "101112131415161718191a1b1c1d1e1f"} ),
|
||||||
"op_opc": h2b("101112131415161718191a1b1c1d1e1f")} ),
|
|
||||||
]
|
]
|
||||||
def __init__(self, fid='6f20', name='EF.SIM_AUTH_KEY'):
|
def __init__(self, fid='6f20', name='EF.SIM_AUTH_KEY'):
|
||||||
super().__init__(fid, name=name, desc='USIM authentication key')
|
super().__init__(fid, name=name, desc='USIM authentication key')
|
||||||
@@ -159,8 +154,8 @@ class EF_SIM_AUTH_KEY(TransparentEF):
|
|||||||
'use_opc_instead_of_op'/Flag,
|
'use_opc_instead_of_op'/Flag,
|
||||||
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
|
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
|
||||||
self._construct = Struct('cfg'/CfgByte,
|
self._construct = Struct('cfg'/CfgByte,
|
||||||
'key'/Bytes(16),
|
'key'/HexAdapter(Bytes(16)),
|
||||||
'op_opc' /Bytes(16))
|
'op_opc' /HexAdapter(Bytes(16)))
|
||||||
|
|
||||||
|
|
||||||
class DF_SYSTEM(CardDF):
|
class DF_SYSTEM(CardDF):
|
||||||
@@ -210,18 +205,6 @@ class EF_USIM_SQN(TransparentEF):
|
|||||||
|
|
||||||
|
|
||||||
class EF_USIM_AUTH_KEY(TransparentEF):
|
class EF_USIM_AUTH_KEY(TransparentEF):
|
||||||
_test_de_encode = [
|
|
||||||
( '141898d827f70120d33b3e7462ee5fd6fe6ca53d7a0a804561646816d7b0c702fb',
|
|
||||||
{ "cfg": { "only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True, "algorithm": "milenage" },
|
|
||||||
"key": h2b("1898d827f70120d33b3e7462ee5fd6fe"), "op_opc": h2b("6ca53d7a0a804561646816d7b0c702fb") } ),
|
|
||||||
( '160a04101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f000102030405060708090a0b0c0d0e0f',
|
|
||||||
{ "cfg" : { "algorithm" : "tuak", "key_length" : 128, "sres_deriv_func_in_2g" : 1, "use_opc_instead_of_op" : True },
|
|
||||||
"tuak_cfg" : { "ck_and_ik_size" : 128, "mac_size" : 128, "res_size" : 128 },
|
|
||||||
"num_of_keccak_iterations" : 4,
|
|
||||||
"k" : h2b("000102030405060708090a0b0c0d0e0f"),
|
|
||||||
"op_opc" : h2b("101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f")
|
|
||||||
} ),
|
|
||||||
]
|
|
||||||
def __init__(self, fid='af20', name='EF.USIM_AUTH_KEY'):
|
def __init__(self, fid='af20', name='EF.USIM_AUTH_KEY'):
|
||||||
super().__init__(fid, name=name, desc='USIM authentication key')
|
super().__init__(fid, name=name, desc='USIM authentication key')
|
||||||
Algorithm = Enum(Nibble, milenage=4, sha1_aka=5, tuak=6, xor=15)
|
Algorithm = Enum(Nibble, milenage=4, sha1_aka=5, tuak=6, xor=15)
|
||||||
@@ -230,8 +213,8 @@ class EF_USIM_AUTH_KEY(TransparentEF):
|
|||||||
'use_opc_instead_of_op'/Mapping(Bit, {False:0, True:1}),
|
'use_opc_instead_of_op'/Mapping(Bit, {False:0, True:1}),
|
||||||
'algorithm'/Algorithm)
|
'algorithm'/Algorithm)
|
||||||
self._construct = Struct('cfg'/CfgByte,
|
self._construct = Struct('cfg'/CfgByte,
|
||||||
'key'/Bytes(16),
|
'key'/HexAdapter(Bytes(16)),
|
||||||
'op_opc'/Bytes(16))
|
'op_opc' /HexAdapter(Bytes(16)))
|
||||||
# TUAK has a rather different layout for the data, so we define a different
|
# TUAK has a rather different layout for the data, so we define a different
|
||||||
# construct below and use explicit _{decode,encode}_bin() methods for separating
|
# construct below and use explicit _{decode,encode}_bin() methods for separating
|
||||||
# the TUAK and non-TUAK situation
|
# the TUAK and non-TUAK situation
|
||||||
@@ -247,8 +230,8 @@ class EF_USIM_AUTH_KEY(TransparentEF):
|
|||||||
self._constr_tuak = Struct('cfg'/CfgByteTuak,
|
self._constr_tuak = Struct('cfg'/CfgByteTuak,
|
||||||
'tuak_cfg'/TuakCfgByte,
|
'tuak_cfg'/TuakCfgByte,
|
||||||
'num_of_keccak_iterations'/Int8ub,
|
'num_of_keccak_iterations'/Int8ub,
|
||||||
'op_opc'/Bytes(32),
|
'op_opc'/HexAdapter(Bytes(32)),
|
||||||
'k'/Bytes(this.cfg.key_length//8))
|
'k'/HexAdapter(Bytes(this.cfg.key_length//8)))
|
||||||
|
|
||||||
def _decode_bin(self, raw_bin_data: bytearray) -> dict:
|
def _decode_bin(self, raw_bin_data: bytearray) -> dict:
|
||||||
if raw_bin_data[0] & 0x0F == 0x06:
|
if raw_bin_data[0] & 0x0F == 0x06:
|
||||||
@@ -256,7 +239,7 @@ class EF_USIM_AUTH_KEY(TransparentEF):
|
|||||||
else:
|
else:
|
||||||
return parse_construct(self._construct, raw_bin_data)
|
return parse_construct(self._construct, raw_bin_data)
|
||||||
|
|
||||||
def _encode_bin(self, abstract_data: dict, **kwargs) -> bytearray:
|
def _encode_bin(self, abstract_data: dict) -> bytearray:
|
||||||
if abstract_data['cfg']['algorithm'] == 'tuak':
|
if abstract_data['cfg']['algorithm'] == 'tuak':
|
||||||
return build_construct(self._constr_tuak, abstract_data)
|
return build_construct(self._constr_tuak, abstract_data)
|
||||||
else:
|
else:
|
||||||
@@ -267,9 +250,8 @@ class EF_USIM_AUTH_KEY_2G(TransparentEF):
|
|||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
||||||
{"cfg": {"only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True,
|
{"cfg": {"only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True,
|
||||||
"algorithm": "milenage"},
|
"algorithm": "milenage"}, "key": "000102030405060708090a0b0c0d0e0f", "op_opc":
|
||||||
"key": h2b("000102030405060708090a0b0c0d0e0f"),
|
"101112131415161718191a1b1c1d1e1f"} ),
|
||||||
"op_opc": h2b("101112131415161718191a1b1c1d1e1f")} ),
|
|
||||||
]
|
]
|
||||||
def __init__(self, fid='af22', name='EF.USIM_AUTH_KEY_2G'):
|
def __init__(self, fid='af22', name='EF.USIM_AUTH_KEY_2G'):
|
||||||
super().__init__(fid, name=name, desc='USIM authentication key in 2G context')
|
super().__init__(fid, name=name, desc='USIM authentication key in 2G context')
|
||||||
@@ -278,8 +260,8 @@ class EF_USIM_AUTH_KEY_2G(TransparentEF):
|
|||||||
'use_opc_instead_of_op'/Flag,
|
'use_opc_instead_of_op'/Flag,
|
||||||
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3, xor=14))
|
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3, xor=14))
|
||||||
self._construct = Struct('cfg'/CfgByte,
|
self._construct = Struct('cfg'/CfgByte,
|
||||||
'key'/Bytes(16),
|
'key'/HexAdapter(Bytes(16)),
|
||||||
'op_opc'/Bytes(16))
|
'op_opc' /HexAdapter(Bytes(16)))
|
||||||
|
|
||||||
|
|
||||||
class EF_GBA_SK(TransparentEF):
|
class EF_GBA_SK(TransparentEF):
|
||||||
@@ -303,9 +285,9 @@ class EF_GBA_INT_KEY(LinFixedEF):
|
|||||||
|
|
||||||
|
|
||||||
class SysmocomSJA2(CardModel):
|
class SysmocomSJA2(CardModel):
|
||||||
_atrs = ["3b9f96801f878031e073fe211b674a4c753034054ba9",
|
_atrs = ["3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9",
|
||||||
"3b9f96801f878031e073fe211b674a4c7531330251b2",
|
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2",
|
||||||
"3b9f96801f878031e073fe211b674a4c5275310451d5"]
|
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_files(cls, rs: RuntimeState):
|
def add_files(cls, rs: RuntimeState):
|
||||||
@@ -334,9 +316,9 @@ class SysmocomSJA2(CardModel):
|
|||||||
isim_adf.add_files(files_adf_isim)
|
isim_adf.add_files(files_adf_isim)
|
||||||
|
|
||||||
class SysmocomSJA5(CardModel):
|
class SysmocomSJA5(CardModel):
|
||||||
_atrs = ["3b9f96801f878031e073fe211b674a357530350251cc",
|
_atrs = ["3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC",
|
||||||
"3b9f96801f878031e073fe211b674a357530350265f8",
|
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8",
|
||||||
"3b9f96801f878031e073fe211b674a357530350259c4"]
|
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_files(cls, rs: RuntimeState):
|
def add_files(cls, rs: RuntimeState):
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user