mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-17 02:48:34 +03:00
Compare commits
1 Commits
laforge/ot
...
laforge/sm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bef85dbc28 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
open_collective: osmocom
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,10 +3,3 @@
|
||||
|
||||
/docs/_*
|
||||
/docs/generated
|
||||
/.cache
|
||||
/.local
|
||||
/build
|
||||
/pySim.egg-info
|
||||
/smdpp-data/sm-dp-sessions
|
||||
dist
|
||||
tags
|
||||
|
||||
52
README.md
52
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
|
||||
subscriber identity modules of cellular networks, including but not limited
|
||||
to SIM, UICC, USIM, ISIM, HPSIMs and eUICCs.
|
||||
|
||||
* `pySim-shell.py` can be used to interactively explore, read and decode contents
|
||||
of any of the supported card models / card applications. Furthermore, if
|
||||
you have the credentials to your card (ADM PIN), you can also write to the card,
|
||||
i.e. edit its contents.
|
||||
* `pySim-read.py` and `pySim-prog.py` are _legacy_ tools for batch programming
|
||||
some very common parameters to an entire batch of programmable cards
|
||||
* `pySim-trace.py` is a tool to do an in-depth decode of SIM card protocol traces
|
||||
such as those obtained by [Osmocom SIMtrace2](https://osmocom.org/projects/simtrace2/wiki)
|
||||
or [osmo-qcdiag](https://osmocom.org/projects/osmo-qcdiag/wiki).
|
||||
* `osmo-smdpp.py` is a proof-of-concept GSMA SGP.22 Consumer eSIM SM-DP+ for lab/research
|
||||
* there are more related tools, particularly in the `contrib` directory.
|
||||
This repository contains a number of Python programs that can be used
|
||||
to read, program (write) and browse all fields/parameters/files on
|
||||
SIM/USIM/ISIM/HPSIM cards used in 3GPP cellular networks from 2G to 5G.
|
||||
|
||||
Note that the access control configuration of normal production cards
|
||||
issue by operators will restrict significantly which files a normal
|
||||
user can read, and particularly write to.
|
||||
|
||||
The full functionality of pySim hence can only be used with on so-called
|
||||
programmable SIM/USIM/ISIM/HPSIM cards, such as the various
|
||||
[sysmocom programmable card products](https://shop.sysmocom.de/SIM/).
|
||||
programmable SIM/USIM/ISIM/HPSIM cards.
|
||||
|
||||
Such SIM/USIM/ISIM/HPSIM cards are special cards, which - unlike those
|
||||
issued by regular commercial operators - come with the kind of keys that
|
||||
@@ -62,9 +49,9 @@ pySim-shell vs. legacy tools
|
||||
----------------------------
|
||||
|
||||
While you will find a lot of online resources still describing the use of
|
||||
`pySim-prog.py` and `pySim-read.py`, those tools are considered legacy by
|
||||
pySim-prog.py and pySim-read.py, those tools are considered legacy by
|
||||
now and have by far been superseded by the much more capable
|
||||
`pySim-shell.py`. We strongly encourage users to adopt pySim-shell, unless
|
||||
pySim-shell. We strongly encourage users to adopt pySim-shell, unless
|
||||
they have very specific requirements like batch programming of large
|
||||
quantities of cards, which is about the only remaining use case for the
|
||||
legacy tools.
|
||||
@@ -90,7 +77,7 @@ Please install the following dependencies:
|
||||
- cmd2 >= 1.5.0
|
||||
- colorlog
|
||||
- construct >= 2.9.51
|
||||
- pyosmocom
|
||||
- gsm0338
|
||||
- jsonpath-ng
|
||||
- packaging
|
||||
- pycryptodomex
|
||||
@@ -136,34 +123,19 @@ sudo pacman -Rs python-pysim-git
|
||||
```
|
||||
|
||||
|
||||
Forum
|
||||
-----
|
||||
|
||||
We welcome any pySim related discussions in the
|
||||
[SIM Card Technology](https://discourse.osmocom.org/c/sim-card-technology/)
|
||||
section of the osmocom discourse (web based Forum).
|
||||
|
||||
|
||||
Mailing List
|
||||
------------
|
||||
|
||||
There is no separate mailing list for this project. However,
|
||||
discussions related to pySim are happening on the simtrace
|
||||
<simtrace@lists.osmocom.org> mailing list, please see
|
||||
<https://lists.osmocom.org/mailman/listinfo/simtrace> for subscription
|
||||
discussions related to pysim-prog are happening on the
|
||||
<openbsc@lists.osmocom.org> mailing list, please see
|
||||
<https://lists.osmocom.org/mailman/listinfo/openbsc> for subscription
|
||||
options and the list archive.
|
||||
|
||||
Please observe the [Osmocom Mailing List
|
||||
Rules](https://osmocom.org/projects/cellular-infrastructure/wiki/Mailing_List_Rules)
|
||||
when posting.
|
||||
|
||||
Issue Tracker
|
||||
-------------
|
||||
|
||||
We use the [issue tracker of the pysim project on osmocom.org](https://osmocom.org/projects/pysim/issues) for
|
||||
tracking the state of bug reports and feature requests. Feel free to submit any issues you may find, or help
|
||||
us out by resolving existing issues.
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Connect smartcard to a remote server, so the remote server can take control and
|
||||
perform commands on it."""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from osmocom.utils import b2h
|
||||
import websockets
|
||||
|
||||
from pySim.transport import init_reader, argparse_add_reader_args, LinkBase
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.wsrc.client_blocking import WsClientBlocking
|
||||
|
||||
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CardWsClientBlocking(WsClientBlocking):
|
||||
"""Implementation of the card (reader) client of the WSRC (WebSocket Remote Card) protocol"""
|
||||
|
||||
def __init__(self, ws_uri, tp: LinkBase):
|
||||
super().__init__(ws_uri)
|
||||
self.tp = tp
|
||||
|
||||
def perform_outbound_hello(self):
|
||||
hello_data = {
|
||||
'client_type': 'card',
|
||||
'atr': b2h(self.tp.get_atr()),
|
||||
# TODO: include various card information in the HELLO message
|
||||
}
|
||||
super().perform_outbound_hello(hello_data)
|
||||
|
||||
def handle_rx_c_apdu(self, rx: dict):
|
||||
"""handle an inbound APDU transceive command"""
|
||||
data, sw = self.tp.send_apdu(rx['command'])
|
||||
tx = {
|
||||
'response': data,
|
||||
'sw': sw,
|
||||
}
|
||||
self.tx_json('r_apdu', tx)
|
||||
|
||||
def handle_rx_disconnect(self, rx: dict):
|
||||
"""server tells us to disconnect"""
|
||||
self.tx_json('disconnect_ack')
|
||||
# FIXME: tear down connection and/or terminate entire program
|
||||
|
||||
def handle_rx_print(self, rx: dict):
|
||||
"""print a message (text) given by server to the local console/log"""
|
||||
print(rx['message'])
|
||||
# no response
|
||||
|
||||
def handle_rx_reset_req(self, rx: dict):
|
||||
"""server tells us to reset the card"""
|
||||
self.tp.reset()
|
||||
self.tx_json('reset_resp', {'atr': b2h(self.tp.get_atr())})
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
argparse_add_reader_args(parser)
|
||||
parser.add_argument("--uri", default="ws://localhost:8765/",
|
||||
help="URI of the sever to which to connect")
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
# open the card reader / slot
|
||||
logger.info("Initializing Card Reader...")
|
||||
tp = init_reader(opts)
|
||||
logger.info("Connecting to Card...")
|
||||
tp.connect()
|
||||
scc = SimCardCommands(transport=tp)
|
||||
logger.info("Detected Card with ATR: %s" % b2h(tp.get_atr()))
|
||||
|
||||
# TODO: gather various information about the card; print it
|
||||
|
||||
# create + connect the client to the server
|
||||
cl = CardWsClientBlocking(opts.uri, tp)
|
||||
logger.info("Connecting to remote server...")
|
||||
try:
|
||||
cl.connect()
|
||||
print("Successfully connected to Server")
|
||||
except ConnectionRefusedError as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# endless loop: wait for inbound command from server + execute it
|
||||
cl.rx_and_execute_cmd()
|
||||
# TODO: clean handling of websocket disconnect
|
||||
except websockets.exceptions.ConnectionClosedOK as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt as e:
|
||||
print(e.__class__.__name__)
|
||||
sys.exit(2)
|
||||
@@ -1,241 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
from websockets.asyncio.server import serve
|
||||
from websockets.exceptions import ConnectionClosedError
|
||||
from osmocom.utils import Hexstr, swap_nibbles
|
||||
|
||||
from pySim.utils import SwMatchstr, ResTuple, sw_match, dec_iccid
|
||||
from pySim.exceptions import SwMatchError
|
||||
|
||||
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
card_clients = set()
|
||||
user_clients = set()
|
||||
|
||||
class WsClient:
|
||||
def __init__(self, websocket, hello: dict):
|
||||
self.websocket = websocket
|
||||
self.hello = hello
|
||||
self.identity = {}
|
||||
|
||||
def __str__(self):
|
||||
return '%s(ws=%s)' % (self.__class__.__name__, self.websocket)
|
||||
|
||||
async def rx_json(self):
|
||||
rx = await self.websocket.recv()
|
||||
rx_js = json.loads(rx)
|
||||
logger.debug("Rx: %s", rx_js)
|
||||
assert 'msg_type' in rx
|
||||
return rx_js
|
||||
|
||||
async def tx_json(self, msg_type:str, d: dict = {}):
|
||||
"""Transmit a json-serializable dict to the peer"""
|
||||
d['msg_type'] = msg_type
|
||||
d_js = json.dumps(d)
|
||||
logger.debug("Tx: %s", d_js)
|
||||
await self.websocket.send(d_js)
|
||||
|
||||
async def tx_hello_ack(self):
|
||||
await self.tx_json('hello_ack')
|
||||
|
||||
async def xceive_json(self, msg_type:str, d:dict = {}, exp_msg_type:Optional[str] = None) -> dict:
|
||||
await self.tx_json(msg_type, d)
|
||||
rx = await self.rx_json()
|
||||
if exp_msg_type:
|
||||
assert rx['msg_type'] == exp_msg_type
|
||||
return rx;
|
||||
|
||||
async def tx_error(self, message: str):
|
||||
"""Transmit an error message to the peer"""
|
||||
event = {
|
||||
"message": message,
|
||||
}
|
||||
await self.tx_json('error', event)
|
||||
|
||||
async def ws_hdlr(self):
|
||||
"""kind of a 'main' function for the websocket client: wait for incoming message,
|
||||
and handle it."""
|
||||
try:
|
||||
async for message in self.websocket:
|
||||
method = getattr(self, 'handle_rx_%s' % message['msg_type'], None)
|
||||
if not method:
|
||||
await self.tx_error("Unknonw msg_type: %s" % message['msg_type'])
|
||||
else:
|
||||
method(message)
|
||||
except ConnectionClosedError:
|
||||
# we handle this in the outer loop
|
||||
pass
|
||||
|
||||
class CardClient(WsClient):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.state = 'init'
|
||||
|
||||
def __str__(self):
|
||||
eid = self.identity.get('EID', None)
|
||||
if eid:
|
||||
return '%s(EID=%s)' % (self.__class__.__name__, eid)
|
||||
iccid = self.identity.get('ICCID', None)
|
||||
if iccid:
|
||||
return '%s(ICCID=%s)' % (self.__class__.__name__, iccid)
|
||||
return super().__str__()
|
||||
|
||||
"""A websocket client that represents a reader/card. This is what we use to talk to a card"""
|
||||
async def xceive_apdu_raw(self, cmd: Hexstr) -> ResTuple:
|
||||
"""transceive a single APDU with the card"""
|
||||
message = await self.xceive_json('c_apdu', {'command': cmd}, 'r_apdu')
|
||||
return message['response'], message['sw']
|
||||
|
||||
async def xceive_apdu(self, pdu: Hexstr) -> ResTuple:
|
||||
"""transceive an APDU with the card, handling T=0 GET_RESPONSE cases"""
|
||||
prev_pdu = pdu
|
||||
data, sw = await self.xceive_apdu_raw(pdu)
|
||||
|
||||
if sw is not None:
|
||||
while (sw[0:2] in ['9f', '61', '62', '63']):
|
||||
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
|
||||
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
|
||||
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
|
||||
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
|
||||
prev_pdu = pdu_gr
|
||||
d, sw = await self.xceive_apdu_raw(pdu_gr)
|
||||
data += d
|
||||
if sw[0:2] == '6c':
|
||||
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
|
||||
pdu_gr = prev_pdu[0:8] + sw[2:4]
|
||||
data, sw = await self.xceive_apdu_raw(pdu_gr)
|
||||
|
||||
return data, sw
|
||||
|
||||
async def xceive_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
|
||||
"""like xceive_apdu, but checking the status word matches the expected pattern"""
|
||||
rv = await self.xceive_apdu(pdu)
|
||||
last_sw = rv[1]
|
||||
|
||||
if not sw_match(rv[1], sw):
|
||||
raise SwMatchError(rv[1], sw.lower())
|
||||
return rv
|
||||
|
||||
async def card_reset(self):
|
||||
"""reset the card"""
|
||||
rx = await self.xceive_json('reset_req', exp_msg_type='reset_resp')
|
||||
|
||||
async def get_iccid(self):
|
||||
"""high-level method to obtain the ICCID of the card"""
|
||||
await self.xceive_apdu_checksw('00a40000023f00') # SELECT MF
|
||||
await self.xceive_apdu_checksw('00a40000022fe2') # SELECT EF.ICCID
|
||||
res, sw = await self.xceive_apdu_checksw('00b0000000') # READ BINARY
|
||||
return dec_iccid(res)
|
||||
|
||||
async def get_eid_sgp22(self):
|
||||
"""high-level method to obtain the EID of a SGP.22 consumer eUICC"""
|
||||
await self.xceive_apdu_checksw('00a4040410a0000005591010ffffffff8900000100')
|
||||
res, sw = await self.xceive_apdu_checksw('80e2910006bf3e035c015a')
|
||||
return res[-32:]
|
||||
|
||||
async def identify(self):
|
||||
# identify the card by asking for its EID and/or ICCID
|
||||
try:
|
||||
eid = await self.get_eid_sgp22()
|
||||
logger.debug("EID: %s", eid)
|
||||
self.identity['EID'] = eid
|
||||
except SwMatchError:
|
||||
pass
|
||||
try:
|
||||
iccid = await self.get_iccid()
|
||||
logger.debug("ICCID: %s", iccid)
|
||||
self.identity['ICCID'] = iccid
|
||||
except SwMatchError:
|
||||
pass
|
||||
logger.info("Card now in READY state")
|
||||
self.state = 'ready'
|
||||
|
||||
@staticmethod
|
||||
def find_client_for_id(id_type: str, id_str: str) -> Optional['CardClient']:
|
||||
for c in card_clients:
|
||||
print("testing card %s in state %s" % (c, c.state))
|
||||
if c.state != 'ready':
|
||||
continue
|
||||
c_id = c.identity.get(id_type.upper(), None)
|
||||
if c_id and c_id.lower() == id_str.lower():
|
||||
return c
|
||||
return None
|
||||
|
||||
class UserClient(WsClient):
|
||||
"""A websocket client representing a user application like pySim-shell."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.state = 'init'
|
||||
|
||||
async def state_init(self):
|
||||
"""Wait for incoming 'select_card' and process it."""
|
||||
while True:
|
||||
rx = await self.rx_json()
|
||||
if rx['msg_type'] == 'select_card':
|
||||
# look-up if the card can be found
|
||||
card = CardClient.find_client_for_id(rx['id_type'], rx['id_str'])
|
||||
if not card:
|
||||
await self.tx_error('No CardClient found for %s == %s' % (rx['id_type'], rx['id_str']))
|
||||
continue
|
||||
# transition to next statee
|
||||
self.state = 'associated'
|
||||
card.state = 'associated'
|
||||
self.card = card
|
||||
await self.tx_json('select_card_ack', {'identities': card.identity})
|
||||
break
|
||||
else:
|
||||
self.tx_error('Unknown message type %s' % rx['msg_type'])
|
||||
|
||||
async def state_selected(self):
|
||||
while True:
|
||||
rx = await self.rx_json()
|
||||
if rx['msg_type'] == 'c_apdu':
|
||||
rsp, sw = await self.card.xceive_apdu_raw(rx['command'])
|
||||
await self.tx_json('r_apdu', {'response': rsp, 'sw': sw})
|
||||
|
||||
|
||||
async def ws_conn_hdlr(websocket):
|
||||
rx_raw = await websocket.recv()
|
||||
rx = json.loads(rx_raw)
|
||||
assert rx['msg_type'] == 'hello'
|
||||
client_type = rx['client_type']
|
||||
logger.info("New client (type %s) connection accepted", client_type)
|
||||
|
||||
if client_type == 'card':
|
||||
card = CardClient(websocket, rx)
|
||||
await card.tx_hello_ack()
|
||||
card_clients.add(card)
|
||||
# first obtain the identity of the card
|
||||
await card.identify()
|
||||
# then go into the "main loop"
|
||||
try:
|
||||
await card.ws_hdlr()
|
||||
finally:
|
||||
logger.info("%s: connection closed", card)
|
||||
card_clients.remove(card)
|
||||
elif client_type == 'user':
|
||||
user = UserClient(websocket, rx)
|
||||
await user.tx_hello_ack()
|
||||
user_clients.add(user)
|
||||
# first wait for the user to specify the select the card
|
||||
await user.state_init()
|
||||
try:
|
||||
await user.state_selected()
|
||||
finally:
|
||||
logger.info("%s: connection closed", user)
|
||||
user_clients.remove(user)
|
||||
else:
|
||||
logger.info("Rejecting client (unknown type %s) connection", client_type)
|
||||
raise ValueError
|
||||
|
||||
|
||||
async def main():
|
||||
async with serve(ws_conn_hdlr, "localhost", 8765):
|
||||
await asyncio.get_running_loop().create_future() # run forever
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Utility program to perform column-based encryption of a CSV file holding SIM/UICC
|
||||
# related key materials.
|
||||
#
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import csv
|
||||
import argparse
|
||||
from Cryptodome.Cipher import AES
|
||||
from osmocom.utils import h2b, b2h, Hexstr
|
||||
|
||||
from pySim.card_key_provider import CardKeyProviderCsv
|
||||
|
||||
def dict_keys_to_upper(d: dict) -> dict:
|
||||
return {k.upper():v for k,v in d.items()}
|
||||
|
||||
class CsvColumnEncryptor:
|
||||
def __init__(self, filename: str, transport_keys: dict):
|
||||
self.filename = filename
|
||||
self.transport_keys = dict_keys_to_upper(transport_keys)
|
||||
|
||||
def encrypt_col(self, colname:str, value: str) -> Hexstr:
|
||||
key = self.transport_keys[colname]
|
||||
cipher = AES.new(h2b(key), AES.MODE_CBC, CardKeyProviderCsv.IV)
|
||||
return b2h(cipher.encrypt(h2b(value)))
|
||||
|
||||
def encrypt(self) -> None:
|
||||
with open(self.filename, 'r') as infile:
|
||||
cr = csv.DictReader(infile)
|
||||
cr.fieldnames = [field.upper() for field in cr.fieldnames]
|
||||
|
||||
with open(self.filename + '.encr', 'w') as outfile:
|
||||
cw = csv.DictWriter(outfile, dialect=csv.unix_dialect, fieldnames=cr.fieldnames)
|
||||
cw.writeheader()
|
||||
|
||||
for row in cr:
|
||||
for key_colname in self.transport_keys:
|
||||
if key_colname in row:
|
||||
row[key_colname] = self.encrypt_col(key_colname, row[key_colname])
|
||||
cw.writerow(row)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('CSVFILE', help="CSV file name")
|
||||
parser.add_argument('--csv-column-key', action='append', required=True,
|
||||
help='per-CSV-column AES transport key')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
csv_column_keys = {}
|
||||
for par in opts.csv_column_key:
|
||||
name, key = par.split(':')
|
||||
csv_column_keys[name] = key
|
||||
|
||||
if len(csv_column_keys) == 0:
|
||||
print("You must specify at least one key!")
|
||||
sys.exit(1)
|
||||
|
||||
csv_column_keys = CardKeyProviderCsv.process_transport_keys(csv_column_keys)
|
||||
for name, key in csv_column_keys.items():
|
||||
print("Encrypting column %s using AES key %s" % (name, key))
|
||||
|
||||
cce = CsvColumnEncryptor(opts.CSVFILE, csv_column_keys)
|
||||
cce.encrypt()
|
||||
@@ -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 manuall issue requests against the ES2+ API of an SM-DP+ according to GSMA SGP.22.""")
|
||||
parser.add_argument('--url', required=True, help='Base URL of ES2+ API endpoint')
|
||||
parser.add_argument('--id', required=True, help='Entity identifier passed to SM-DP+')
|
||||
parser.add_argument('--client-cert', help='X.509 client certificate used to authenticate to server')
|
||||
parser.add_argument('--server-ca-cert', help="""X.509 CA certificates acceptable for the server side. In
|
||||
production use cases, this would be the GSMA Root CA (CI) certificate.""")
|
||||
subparsers = parser.add_subparsers(dest='command',help="The command (API function) to call")
|
||||
|
||||
parser_dlo = subparsers.add_parser('download-order', help="ES2+ DownloadOrder function")
|
||||
parser_dlo.add_argument('--eid', help=EID_HELP)
|
||||
parser_dlo.add_argument('--iccid', help=ICCID_HELP)
|
||||
parser_dlo.add_argument('--profileType', help='The profile type of which one eSIM shall be made available')
|
||||
|
||||
parser_cfo = subparsers.add_parser('confirm-order', help="ES2+ ConfirmOrder function")
|
||||
parser_cfo.add_argument('--iccid', required=True, help=ICCID_HELP)
|
||||
parser_cfo.add_argument('--eid', help=EID_HELP)
|
||||
parser_cfo.add_argument('--matchingId', help=MATCHID_HELP)
|
||||
parser_cfo.add_argument('--confirmationCode', help='Confirmation code that shall be used by profile download')
|
||||
parser_cfo.add_argument('--smdsAddress', help='SM-DS Address')
|
||||
parser_cfo.add_argument('--releaseFlag', action='store_true', help='Shall the profile be immediately released?')
|
||||
|
||||
parser_co = subparsers.add_parser('cancel-order', help="ES2+ CancelOrder function")
|
||||
parser_co.add_argument('--iccid', required=True, help=ICCID_HELP)
|
||||
parser_co.add_argument('--eid', help=EID_HELP)
|
||||
parser_co.add_argument('--matchingId', help=MATCHID_HELP)
|
||||
parser_co.add_argument('--finalProfileStatusIndicator', required=True, choices=['Available','Unavailable'])
|
||||
|
||||
parser_rp = subparsers.add_parser('release-profile', help='ES2+ ReleaseProfile function')
|
||||
parser_rp.add_argument('--iccid', required=True, help=ICCID_HELP)
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
#print(opts)
|
||||
|
||||
peer = es2p.Es2pApiClient(opts.url, opts.id, server_cert_verify=opts.server_ca_cert, client_cert=opts.client_cert)
|
||||
|
||||
data = {}
|
||||
for k, v in vars(opts).items():
|
||||
if k in ['url', 'id', 'client_cert', 'server_ca_cert', 'command']:
|
||||
# remove keys from dict that shold not end up in JSON...
|
||||
continue
|
||||
if v is not None:
|
||||
data[k] = v
|
||||
|
||||
print(data)
|
||||
if opts.command == 'download-order':
|
||||
res = peer.call_downloadOrder(data)
|
||||
elif opts.command == 'confirm-order':
|
||||
res = peer.call_confirmOrder(data)
|
||||
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 Opreation whoise occurrence shall be notififed')
|
||||
parser_ntf.add_argument('--sequence-nr', type=int, required=True,
|
||||
help='eUICC global notification sequence number')
|
||||
parser_ntf.add_argument('--notification-address', help='notificationAddress, if different from URL')
|
||||
parser_ntf.add_argument('--iccid', type=is_hexstr, help='ICCID to which the notification relates')
|
||||
|
||||
# notification-install
|
||||
parser_ntfi = subparsers.add_parser('notification-install', help='ES9+ installation notification')
|
||||
parser_ntfi.add_argument('--sequence-nr', type=int, required=True,
|
||||
help='eUICC global notification sequence number')
|
||||
parser_ntfi.add_argument('--transaction-id', required=True,
|
||||
help='transactionId of previous ES9+ download')
|
||||
parser_ntfi.add_argument('--notification-address', help='notificationAddress, if different from URL')
|
||||
parser_ntfi.add_argument('--iccid', type=is_hexstr, help='ICCID to which the notification relates')
|
||||
parser_ntfi.add_argument('--smdpp-oid', required=True, help='SM-DP+ OID (as in CERT.DPpb.ECDSA)')
|
||||
parser_ntfi.add_argument('--isdp-aid', type=is_hexstr, required=True,
|
||||
help='AID of the ISD-P of the installed profile')
|
||||
parser_ntfi.add_argument('--sima-response', type=is_hexstr, required=True,
|
||||
help='hex digits of BER-encoded SAIP EUICCResponse')
|
||||
|
||||
class Es9pClient:
|
||||
def __init__(self, opts):
|
||||
self.opts = opts
|
||||
self.cert_and_key = CertAndPrivkey()
|
||||
self.cert_and_key.cert_from_der_file(os.path.join(opts.certificate_path, opts.euicc_certificate))
|
||||
self.cert_and_key.privkey_from_pem_file(os.path.join(opts.certificate_path, opts.euicc_private_key))
|
||||
|
||||
with open(os.path.join(opts.certificate_path, opts.eum_certificate), 'rb') as f:
|
||||
self.eum_cert = x509.load_der_x509_certificate(f.read())
|
||||
|
||||
with open(os.path.join(opts.certificate_path, opts.ci_certificate), 'rb') as f:
|
||||
self.ci_cert = x509.load_der_x509_certificate(f.read())
|
||||
subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), self.ci_cert.extensions))
|
||||
subject_pkid = subject_exts[0].value
|
||||
self.ci_pkid = subject_pkid.key_identifier
|
||||
|
||||
print("EUICC: %s" % self.cert_and_key.cert.subject)
|
||||
print("EUM: %s" % self.eum_cert.subject)
|
||||
print("CI: %s" % self.ci_cert.subject)
|
||||
|
||||
self.eid = self.cert_and_key.cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
|
||||
print("EID: %s" % self.eid)
|
||||
print("CI PKID: %s" % b2h(self.ci_pkid))
|
||||
print()
|
||||
|
||||
self.peer = es9p.Es9pApiClient(opts.url, server_cert_verify=opts.server_ca_cert)
|
||||
|
||||
|
||||
def do_notification(self):
|
||||
|
||||
ntf_metadata = {
|
||||
'seqNumber': self.opts.sequence_nr,
|
||||
'profileManagementOperation': PMO(self.opts.operation).to_bitstring(),
|
||||
'notificationAddress': self.opts.notification_address or urlparse(self.opts.url).netloc,
|
||||
}
|
||||
if opts.iccid:
|
||||
ntf_metadata['iccid'] = h2b(swap_nibbles(opts.iccid))
|
||||
|
||||
if self.opts.operation == 'download':
|
||||
pird = {
|
||||
'transactionId': self.opts.transaction_id,
|
||||
'notificationMetadata': ntf_metadata,
|
||||
'smdpOid': self.opts.smdpp_oid,
|
||||
'finalResult': ('successResult', {
|
||||
'aid': self.opts.isdp_aid,
|
||||
'simaResponse': self.opts.sima_response,
|
||||
}),
|
||||
}
|
||||
pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
|
||||
signature = self.cert_and_key.ecdsa_sign(pird_bin)
|
||||
pn_dict = ('profileInstallationResult', {
|
||||
'profileInstallationResultData': pird,
|
||||
'euiccSignPIR': signature,
|
||||
})
|
||||
else:
|
||||
ntf_bin = rsp.asn1.encode('NotificationMetadata', ntf_metadata)
|
||||
signature = self.cert_and_key.ecdsa_sign(ntf_bin)
|
||||
pn_dict = ('otherSignedNotification', {
|
||||
'tbsOtherNotification': ntf_metadata,
|
||||
'euiccNotificationSignature': signature,
|
||||
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
|
||||
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER)),
|
||||
})
|
||||
|
||||
data = {
|
||||
'pendingNotification': pn_dict,
|
||||
}
|
||||
#print(data)
|
||||
res = self.peer.call_handleNotification(data)
|
||||
|
||||
|
||||
def do_download(self):
|
||||
|
||||
print("Step 1: InitiateAuthentication...")
|
||||
|
||||
euiccInfo1 = {
|
||||
'svn': b'\x02\x04\x00',
|
||||
'euiccCiPKIdListForVerification': [
|
||||
self.ci_pkid,
|
||||
],
|
||||
'euiccCiPKIdListForSigning': [
|
||||
self.ci_pkid,
|
||||
],
|
||||
}
|
||||
|
||||
data = {
|
||||
'euiccChallenge': os.urandom(16),
|
||||
'euiccInfo1': euiccInfo1,
|
||||
'smdpAddress': urlparse(self.opts.url).netloc,
|
||||
}
|
||||
init_auth_res = self.peer.call_initiateAuthentication(data)
|
||||
print(init_auth_res)
|
||||
|
||||
print("Step 2: AuthenticateClient...")
|
||||
|
||||
#res['serverSigned1']
|
||||
#res['serverSignature1']
|
||||
print("TODO: verify serverSignature1 over serverSigned1")
|
||||
#res['transactionId']
|
||||
print("TODO: verify transactionId matches the signed one in serverSigned1")
|
||||
#res['euiccCiPKIdToBeUsed']
|
||||
# TODO: select eUICC certificate based on CI
|
||||
#res['serverCertificate']
|
||||
# TODO: verify server certificate against CI
|
||||
|
||||
euiccInfo2 = {
|
||||
'profileVersion': b'\x02\x03\x01',
|
||||
'svn': euiccInfo1['svn'],
|
||||
'euiccFirmwareVer': b'\x23\x42\x00',
|
||||
'extCardResource': b'\x81\x01\x00\x82\x04\x00\x04\x9ch\x83\x02"#',
|
||||
'uiccCapability': (b'k6\xd3\xc3', 32),
|
||||
'javacardVersion': b'\x11\x02\x00',
|
||||
'globalplatformVersion': b'\x02\x03\x00',
|
||||
'rspCapability': (b'\x9c', 6),
|
||||
'euiccCiPKIdListForVerification': euiccInfo1['euiccCiPKIdListForVerification'],
|
||||
'euiccCiPKIdListForSigning': euiccInfo1['euiccCiPKIdListForSigning'],
|
||||
#'euiccCategory':
|
||||
#'forbiddenProfilePolicyRules':
|
||||
'ppVersion': b'\x01\x00\x00',
|
||||
'sasAcreditationNumber': 'OSMOCOM-TEST-1', #TODO: make configurable
|
||||
#'certificationDataObject':
|
||||
}
|
||||
|
||||
euiccSigned1 = {
|
||||
'transactionId': h2b(init_auth_res['transactionId']),
|
||||
'serverAddress': init_auth_res['serverSigned1']['serverAddress'],
|
||||
'serverChallenge': init_auth_res['serverSigned1']['serverChallenge'],
|
||||
'euiccInfo2': euiccInfo2,
|
||||
'ctxParams1':
|
||||
('ctxParamsForCommonAuthentication', {
|
||||
'matchingId': self.opts.matchingId,
|
||||
'deviceInfo': {
|
||||
'tac': b'\x35\x23\x01\x45', # same as lpac
|
||||
'deviceCapabilities': {},
|
||||
#imei:
|
||||
}
|
||||
}),
|
||||
}
|
||||
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
|
||||
euiccSignature1 = self.cert_and_key.ecdsa_sign(euiccSigned1_bin)
|
||||
auth_clnt_req = {
|
||||
'transactionId': init_auth_res['transactionId'],
|
||||
'authenticateServerResponse':
|
||||
('authenticateResponseOk', {
|
||||
'euiccSigned1': euiccSigned1,
|
||||
'euiccSignature1': euiccSignature1,
|
||||
'euiccCertificate': rsp.asn1.decode('Certificate', self.cert_and_key.get_cert_as_der()),
|
||||
'eumCertificate': rsp.asn1.decode('Certificate', self.eum_cert.public_bytes(Encoding.DER))
|
||||
})
|
||||
}
|
||||
auth_clnt_res = self.peer.call_authenticateClient(auth_clnt_req)
|
||||
print(auth_clnt_res)
|
||||
#auth_clnt_res['transactionId']
|
||||
print("TODO: verify transactionId matches previous ones")
|
||||
#auth_clnt_res['profileMetadata']
|
||||
# TODO: what's in here?
|
||||
#auth_clnt_res['smdpSigned2']['bppEuiccOtpk']
|
||||
#auth_clnt_res['smdpSignature2']
|
||||
print("TODO: verify serverSignature2 over smdpSigned2")
|
||||
|
||||
smdp_cert = x509.load_der_x509_certificate(auth_clnt_res['smdpCertificate'])
|
||||
|
||||
print("Step 3: GetBoundProfilePackage...")
|
||||
# Generate a one-time ECKA key pair (ot{PK,SK}.DP.ECKA) using the curve indicated by the Key Parameter
|
||||
# Reference value of CERT.DPpb.ECDSA
|
||||
euicc_ot = ec.generate_private_key(smdp_cert.public_key().public_numbers().curve)
|
||||
|
||||
# extract the public key in (hopefully) the right format for the ES8+ interface
|
||||
euicc_otpk = euicc_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
|
||||
|
||||
euiccSigned2 = {
|
||||
'transactionId': h2b(auth_clnt_res['transactionId']),
|
||||
'euiccOtpk': euicc_otpk,
|
||||
#hashCC
|
||||
}
|
||||
# check for smdpSigned2 ccRequiredFlag, and send it in PrepareDownloadRequest hashCc
|
||||
if auth_clnt_res['smdpSigned2']['ccRequiredFlag']:
|
||||
if not self.opts.confirmation_code:
|
||||
raise ValueError('Confirmation Code required but not provided')
|
||||
cc_hash = hashlib.sha256(self.opts.confirmation_code.encode('ascii')).digest()
|
||||
euiccSigned2['hashCc'] = hashlib.sha256(cc_hash + euiccSigned2['transactionId']).digest()
|
||||
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
|
||||
euiccSignature2 = self.cert_and_key.ecdsa_sign(euiccSigned2_bin + auth_clnt_res['smdpSignature2'])
|
||||
gbp_req = {
|
||||
'transactionId': auth_clnt_res['transactionId'],
|
||||
'prepareDownloadResponse':
|
||||
('downloadResponseOk', {
|
||||
'euiccSigned2': euiccSigned2,
|
||||
'euiccSignature2': euiccSignature2,
|
||||
})
|
||||
}
|
||||
gbp_res = self.peer.call_getBoundProfilePackage(gbp_req)
|
||||
print(gbp_res)
|
||||
#gbp_res['transactionId']
|
||||
# TODO: verify transactionId
|
||||
print("TODO: verify transactionId matches previous ones")
|
||||
bpp_bin = gbp_res['boundProfilePackage']
|
||||
print("TODO: verify boundProfilePackage smdpSignature")
|
||||
|
||||
bpp = BoundProfilePackage()
|
||||
upp_bin = bpp.decode(euicc_ot, self.eid, bpp_bin)
|
||||
|
||||
iccid = swap_nibbles(b2h(bpp.storeMetadataRequest['iccid']))
|
||||
base_name = os.path.join(self.opts.output_path, '%s' % iccid)
|
||||
|
||||
print("SUCCESS: Storing files as %s.*.der" % base_name)
|
||||
|
||||
# write various output files
|
||||
with open(base_name+'.upp.der', 'wb') as f:
|
||||
f.write(bpp.upp)
|
||||
with open(base_name+'.isdp.der', 'wb') as f:
|
||||
f.write(bpp.encoded_configureISDPRequest)
|
||||
with open(base_name+'.smr.der', 'wb') as f:
|
||||
f.write(bpp.encoded_storeMetadataRequest)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
c = Es9pClient(opts)
|
||||
|
||||
if opts.command == 'download':
|
||||
c.do_download()
|
||||
elif opts.command == 'notification':
|
||||
c.do_notification()
|
||||
elif opts.command == 'notification-install':
|
||||
opts.operation = 'install'
|
||||
c.do_notification()
|
||||
@@ -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,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,206 +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/>.
|
||||
|
||||
|
||||
# This is a script to generate a [partial] eSIM profile from the 'fsdump' of another USIM/ISIM. This is
|
||||
# useful to generate an "as close as possible" eSIM from a physical USIM, as far as that is possible
|
||||
# programmatically and in a portable way.
|
||||
#
|
||||
# Of course, this really only affects the file sytem aspects of the card. It's not possible
|
||||
# to read the K/OPc or other authentication related parameters off a random USIM, and hence
|
||||
# we cannot replicate that. Similarly, it's not possible to export the java applets from a USIM,
|
||||
# and hence we cannot replicate those.
|
||||
|
||||
import argparse
|
||||
|
||||
from pySim.esim.saip import *
|
||||
from pySim.ts_102_221 import *
|
||||
|
||||
class FsdumpToSaip:
|
||||
def __init__(self, pes: ProfileElementSequence):
|
||||
self.pes = pes
|
||||
|
||||
@staticmethod
|
||||
def fcp_raw2saip_fcp(fname:str, fcp_raw: bytes) -> Dict:
|
||||
"""Convert a raw TLV-encoded FCP to a SAIP dict-format (as needed by asn1encode)."""
|
||||
ftype = fname.split('.')[0]
|
||||
# use the raw FCP as basis so we don't get stuck with potentially old decoder bugs
|
||||
# ore future format incompatibilities
|
||||
fcp = FcpTemplate()
|
||||
fcp.from_tlv(fcp_raw)
|
||||
r = {}
|
||||
|
||||
r['fileDescriptor'] = fcp.child_by_type(FileDescriptor).to_bytes()
|
||||
|
||||
file_id = fcp.child_by_type(FileIdentifier)
|
||||
if file_id:
|
||||
r['fileID'] = file_id.to_bytes()
|
||||
else:
|
||||
if ftype in ['ADF']:
|
||||
print('%s is an ADF but has no [mandatory] file_id!' % fname)
|
||||
#raise ValueError('%s is an ADF but has no [mandatory] file_id!' % fname)
|
||||
r['fileID'] = b'\x7f\xff' # FIXME: auto-increment
|
||||
|
||||
df_name = fcp.child_by_type(DfName)
|
||||
if ftype in ['ADF']:
|
||||
if not df_name:
|
||||
raise ValueError('%s is an ADF but has no [mandatory] df_name!' % fname)
|
||||
r['dfName'] = df_name.to_bytes()
|
||||
|
||||
lcsi_byte = fcp.child_by_type(LifeCycleStatusInteger).to_bytes()
|
||||
if lcsi_byte != b'\x05':
|
||||
r['lcsi'] = lcsi_byte
|
||||
|
||||
sa_ref = fcp.child_by_type(SecurityAttribReferenced)
|
||||
if sa_ref:
|
||||
r['securityAttributesReferenced'] = sa_ref.to_bytes()
|
||||
|
||||
file_size = fcp.child_by_type(FileSize)
|
||||
if ftype in ['EF']:
|
||||
if file_size:
|
||||
r['efFileSize'] = file_size.to_bytes()
|
||||
|
||||
psdo = fcp.child_by_type(PinStatusTemplate_DO)
|
||||
if ftype in ['MF', 'ADF', 'DF']:
|
||||
if not psdo:
|
||||
raise ValueError('%s is an %s but has no [mandatory] PinStatusTemplateDO' % fname)
|
||||
else:
|
||||
r['pinStatusTemplateDO'] = psdo.to_bytes()
|
||||
|
||||
sfid = fcp.child_by_type(ShortFileIdentifier)
|
||||
if sfid and sfid.decoded:
|
||||
if ftype not in ['EF']:
|
||||
raise ValueError('%s is not an EF but has [forbidden] shortEFID' % fname)
|
||||
r['shortEFID'] = sfid.to_bytes()
|
||||
|
||||
pinfo = fcp.child_by_type(ProprietaryInformation)
|
||||
if pinfo and ftype in ['EF']:
|
||||
spinfo = pinfo.child_by_type(SpecialFileInfo)
|
||||
fill_p = pinfo.child_by_type(FillingPattern)
|
||||
repeat_p = pinfo.child_by_type(RepeatPattern)
|
||||
# only exists for BER-TLV files
|
||||
max_fsize = pinfo.child_by_type(MaximumFileSize)
|
||||
|
||||
if spinfo or fill_p or repeat_p or max_fsize:
|
||||
r['proprietaryEFInfo'] = {}
|
||||
if spinfo:
|
||||
r['proprietaryEFInfo']['specialFileInformation'] = spinfo.to_bytes()
|
||||
if fill_p:
|
||||
r['proprietaryEFInfo']['fillPattern'] = fill_p.to_bytes()
|
||||
if repeat_p:
|
||||
r['proprietaryEFInfo']['repeatPattern'] = repeat_p.to_bytes()
|
||||
if max_fsize:
|
||||
r['proprietaryEFInfo']['maximumFileSize'] = max_fsize.to_bytes()
|
||||
|
||||
# TODO: linkPath
|
||||
return r
|
||||
|
||||
@staticmethod
|
||||
def fcp_fsdump2saip(fsdump_ef: Dict):
|
||||
"""Convert a file from its "fsdump" representation to the SAIP representation of a File type
|
||||
in the decoded format as used by the asn1tools-generated codec."""
|
||||
# first convert the FCP
|
||||
path = fsdump_ef['path']
|
||||
fdesc = FsdumpToSaip.fcp_raw2saip_fcp(path[-1], h2b(fsdump_ef['fcp_raw']))
|
||||
r = [
|
||||
('fileDescriptor', fdesc),
|
||||
]
|
||||
# then convert the body. We're doing a straight-forward conversion without any optimization
|
||||
# like not encoding all-ff files. This should be done by a subsequent optimizer
|
||||
if 'body' in fsdump_ef and fsdump_ef['body']:
|
||||
if isinstance(fsdump_ef['body'], list):
|
||||
for b_seg in fsdump_ef['body']:
|
||||
r.append(('fillFileContent', h2b(b_seg)))
|
||||
else:
|
||||
r.append(('fillFileContent', h2b(fsdump_ef['body'])))
|
||||
print(fsdump_ef['path'])
|
||||
return r
|
||||
|
||||
def add_file_from_fsdump(self, fsdump_ef: Dict):
|
||||
fid = int(fsdump_ef['fcp']['file_identifier'])
|
||||
# determine NAA
|
||||
if fsdump_ef['path'][0:1] == ['MF', 'ADF.USIM']:
|
||||
naa = NaaUsim
|
||||
elif fsdump_ef['path'][0:1] == ['MF', 'ADF.ISIM']:
|
||||
naa = NaaIsim
|
||||
else:
|
||||
print("Unable to determine NAA for %s" % fsdump_ef['path'])
|
||||
return
|
||||
pes.pes_by_naa[naa.name]
|
||||
for pe in pes:
|
||||
print("PE %s" % pe)
|
||||
if not isinstance(pe, FsProfileElement):
|
||||
print("Skipping PE %s" % pe)
|
||||
continue
|
||||
if not pe.template:
|
||||
print("No template for PE %s" % pe )
|
||||
continue
|
||||
if not fid in pe.template.files_by_fid:
|
||||
print("File %04x not available in template; must create it using GenericFileMgmt" % fid)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('fsdump', help='')
|
||||
|
||||
def has_unsupported_path_prefix(path: List[str]) -> bool:
|
||||
# skip some paths from export as they don't exist in an eSIM profile
|
||||
UNSUPPORTED_PATHS = [
|
||||
['MF', 'DF.GSM'],
|
||||
]
|
||||
for p in UNSUPPORTED_PATHS:
|
||||
prefix = path[:len(p)]
|
||||
if prefix == p:
|
||||
return True
|
||||
# any ADF not USIM or ISIM are unsupported
|
||||
SUPPORTED_ADFS = [ 'ADF.USIM', 'ADF.ISIM' ]
|
||||
if len(path) == 2 and path[0] == 'MF' and path[1].startswith('ADF.') and path[1] not in SUPPORTED_ADFS:
|
||||
return True
|
||||
return False
|
||||
|
||||
import traceback
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
with open(opts.fsdump, 'r') as f:
|
||||
fsdump = json.loads(f.read())
|
||||
|
||||
pes = ProfileElementSequence()
|
||||
|
||||
# FIXME: fsdump has strting-name path, but we need FID-list path for ProfileElementSequence
|
||||
for path, fsdump_ef in fsdump['files'].items():
|
||||
print("=" * 80)
|
||||
#print(fsdump_ef)
|
||||
if not 'fcp_raw' in fsdump_ef:
|
||||
continue
|
||||
if has_unsupported_path_prefix(fsdump_ef['path']):
|
||||
print("Skipping eSIM-unsupported path %s" % ('/'.join(fsdump_ef['path'])))
|
||||
continue
|
||||
saip_dec = FsdumpToSaip.fcp_fsdump2saip(fsdump_ef)
|
||||
#print(saip_dec)
|
||||
try:
|
||||
f = pes.add_file_at_path(Path(path), saip_dec)
|
||||
print(repr(f))
|
||||
except Exception as e:
|
||||
print("EXCEPTION: %s" % traceback.format_exc())
|
||||
#print("EXCEPTION: %s" % e)
|
||||
continue
|
||||
|
||||
print("=== Tree === ")
|
||||
pes.mf.print_tree()
|
||||
|
||||
# FIXME: export the actual PE Sequence
|
||||
|
||||
@@ -4,23 +4,18 @@
|
||||
# environment variables:
|
||||
# * WITH_MANUALS: build manual PDFs if set to "1"
|
||||
# * PUBLISH: upload manuals after building if set to "1" (ignored without WITH_MANUALS = "1")
|
||||
# * JOB_TYPE: one of 'test', 'distcheck', 'pylint', 'docs'
|
||||
# * SKIP_CLEAN_WORKSPACE: don't run osmo-clean-workspace.sh (for pyosmocom CI)
|
||||
# * JOB_TYPE: one of 'test', 'pylint', 'docs'
|
||||
#
|
||||
|
||||
export PYTHONUNBUFFERED=1
|
||||
|
||||
if [ ! -d "./tests/" ] ; then
|
||||
if [ ! -d "./pysim-testdata/" ] ; then
|
||||
echo "###############################################"
|
||||
echo "Please call from pySim-prog top directory"
|
||||
echo "###############################################"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SKIP_CLEAN_WORKSPACE" ]; then
|
||||
osmo-clean-workspace.sh
|
||||
fi
|
||||
|
||||
case "$JOB_TYPE" in
|
||||
"test")
|
||||
virtualenv -p python3 venv --system-site-packages
|
||||
@@ -30,39 +25,16 @@ case "$JOB_TYPE" in
|
||||
pip install pyshark
|
||||
|
||||
# Execute automatically discovered unit tests first
|
||||
python -m unittest discover -v -s tests/unittests
|
||||
python -m unittest discover -v -s tests/
|
||||
|
||||
# Run pySim-prog integration tests (requires physical cards)
|
||||
cd tests/pySim-prog_test/
|
||||
./pySim-prog_test.sh
|
||||
cd ../../
|
||||
|
||||
# Run pySim-trace test
|
||||
tests/pySim-trace_test/pySim-trace_test.sh
|
||||
|
||||
# Run pySim-shell integration tests (requires physical cards)
|
||||
python3 -m unittest discover -v -s ./tests/pySim-shell_test/
|
||||
;;
|
||||
"distcheck")
|
||||
virtualenv -p python3 venv --system-site-packages
|
||||
. venv/bin/activate
|
||||
|
||||
pip install .
|
||||
pip install pyshark
|
||||
|
||||
for prog in venv/bin/pySim-*.py; do
|
||||
$prog --help > /dev/null
|
||||
done
|
||||
# Run the test with physical cards
|
||||
cd pysim-testdata
|
||||
../tests/pySim-prog_test.sh
|
||||
../tests/pySim-trace_test.sh
|
||||
;;
|
||||
"pylint")
|
||||
# Print pylint version
|
||||
pip3 freeze | grep pylint
|
||||
|
||||
virtualenv -p python3 venv --system-site-packages
|
||||
. venv/bin/activate
|
||||
|
||||
pip install .
|
||||
|
||||
# Run pylint to find potential errors
|
||||
# Ignore E1102: not-callable
|
||||
# pySim/filesystem.py: E1102: method is not callable (not-callable)
|
||||
@@ -73,19 +45,9 @@ case "$JOB_TYPE" in
|
||||
--disable E1102 \
|
||||
--disable E0401 \
|
||||
--enable W0301 \
|
||||
pySim tests/unittests/*.py *.py \
|
||||
contrib/*.py
|
||||
pySim *.py
|
||||
;;
|
||||
"docs")
|
||||
virtualenv -p python3 venv --system-site-packages
|
||||
. venv/bin/activate
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
# XXX: workaround for https://github.com/python-cmd2/cmd2/issues/1414
|
||||
# 2.4.3 was the last stable release not affected by this bug (OS#6776)
|
||||
pip install cmd2==2.4.3
|
||||
|
||||
rm -rf docs/_build
|
||||
make -C "docs" html latexpdf
|
||||
|
||||
@@ -98,5 +60,3 @@ case "$JOB_TYPE" in
|
||||
echo "ERROR: JOB_TYPE has unexpected value '$JOB_TYPE'."
|
||||
exit 1
|
||||
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 speciifed NAAs from PE-Sequence')
|
||||
parser_rn.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_rn.add_argument('--naa-type', required=True, choices=NAAs.keys(), help='Network Access Application type to remove')
|
||||
# TODO: add an --naa-index or the like, so only one given instance can be removed
|
||||
|
||||
parser_info = subparsers.add_parser('info', help='Display information about the profile')
|
||||
parser_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("\tKVN=0x%02x, KID=0x%02x, %s" % (key.key_version_number, key.key_identifier, key.key_components))
|
||||
|
||||
# RFM
|
||||
print()
|
||||
rfms = pes.pe_by_type.get('rfm', [])
|
||||
print("Number of RFM instances: %u" % len(rfms))
|
||||
for rfm in rfms:
|
||||
inst_aid = rfm.decoded['instanceAID']
|
||||
print("RFM instanceAID: %s" % b2h(inst_aid))
|
||||
print("\tMSL: 0x%02x" % rfm.decoded['minimumSecurityLevel'][0])
|
||||
adf = rfm.decoded.get('adfRFMAccess', None)
|
||||
if adf:
|
||||
print("\tADF AID: %s" % b2h(adf['adfAID']))
|
||||
tar_list = rfm.decoded.get('tarList', [inst_aid[-3:]])
|
||||
for tar in tar_list:
|
||||
print("\tTAR: %s" % b2h(tar))
|
||||
|
||||
def do_extract_apps(pes:ProfileElementSequence, opts):
|
||||
apps = pes.pe_by_type.get('application', [])
|
||||
for app_pe in apps:
|
||||
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
|
||||
fname = os.path.join(opts.output_dir, '%s-%s.%s' % (pes.iccid, package_aid, opts.format))
|
||||
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 explaination 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("-n", "--slot-nr", help="SIM slot number", type=int, default=0)
|
||||
subp = parser.add_subparsers()
|
||||
subp.required = True
|
||||
|
||||
auth_p = subp.add_parser('auth', help='UMTS AKA Authentication')
|
||||
auth_p.add_argument("-c", "--count", help="Auth count", type=int, default=10)
|
||||
|
||||
@@ -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)
|
||||
@@ -6,8 +6,7 @@
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from osmocom.utils import b2h, h2b
|
||||
from osmocom.tlv import bertlv_parse_one, bertlv_encode_tag
|
||||
from pySim.utils import bertlv_parse_one, bertlv_encode_tag, b2h, h2b
|
||||
|
||||
def process_one_level(content: bytes, indent: int):
|
||||
remainder = content
|
||||
@@ -36,8 +35,5 @@ if __name__ == '__main__':
|
||||
content = f.read()
|
||||
elif opts.hex:
|
||||
content = h2b(opts.hex)
|
||||
else:
|
||||
# avoid pylint "(possibly-used-before-assignment)" below
|
||||
sys.exit(2)
|
||||
|
||||
process_one_level(content, 0)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= python3 -m sphinx.cmd.build
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
|
||||
@@ -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,125 +0,0 @@
|
||||
Retrieving card-individual keys via CardKeyProvider
|
||||
===================================================
|
||||
|
||||
When working with a batch of cards, or more than one card in general, it
|
||||
is a lot of effort to manually retrieve the card-specific PIN (like
|
||||
ADM1) or key material (like SCP02/SCP03 keys).
|
||||
|
||||
To increase productivity in that regard, pySim has a concept called the
|
||||
`CardKeyProvider`. This is a generic mechanism by which different parts
|
||||
of the pySim[-shell] code can programmatically request card-specific key material
|
||||
from some data source (*provider*).
|
||||
|
||||
For example, when you want to verify the ADM1 PIN using the `verify_adm`
|
||||
command without providing an ADM1 value yourself, pySim-shell will
|
||||
request the ADM1 value for the ICCID of the card via the
|
||||
CardKeyProvider.
|
||||
|
||||
There can in theory be multiple different CardKeyProviders. You can for
|
||||
example develop your own CardKeyProvider that queries some kind of
|
||||
database for the key material, or that uses a key derivation function to
|
||||
derive card-specific key material from a global master key.
|
||||
|
||||
The only actual CardKeyProvider implementation included in pySim is the
|
||||
`CardKeyProviderCsv` which retrieves the key material from a
|
||||
[potentially encrypted] CSV file.
|
||||
|
||||
|
||||
The CardKeyProviderCsv
|
||||
----------------------
|
||||
|
||||
The `CardKeyProviderCsv` allows you to retrieve card-individual key
|
||||
material from a CSV (comma separated value) file that is accessible to pySim.
|
||||
|
||||
The CSV file must have the expected column names, for example `ICCID`
|
||||
and `ADM1` in case you would like to use that CSV to obtain the
|
||||
card-specific ADM1 PIN when using the `verify_adm` command.
|
||||
|
||||
You can specify the CSV file to use via the `--csv` command-line option
|
||||
of pySim-shell. If you do not specify a CSV file, pySim will attempt to
|
||||
open a CSV file from the default location at
|
||||
`~/.osmocom/pysim/card_data.csv`, and use that, if it exists.
|
||||
|
||||
Column-Level CSV encryption
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
pySim supports column-level CSV encryption. This feature will make sure
|
||||
that your key material is not stored in plaintext in the CSV file.
|
||||
|
||||
The encryption mechanism uses AES in CBC mode. You can use any key
|
||||
length permitted by AES (128/192/256 bit).
|
||||
|
||||
Following GSMA FS.28, the encryption works on column level. This means
|
||||
different columns can be decrypted using different key material. This
|
||||
means that leakage of a column encryption key for one column or set of
|
||||
columns (like a specific security domain) does not compromise various
|
||||
other keys that might be stored in other columns.
|
||||
|
||||
You can specify column-level decryption keys using the
|
||||
`--csv-column-key` command line argument. The syntax is
|
||||
`FIELD:AES_KEY_HEX`, for example:
|
||||
|
||||
`pySim-shell.py --csv-column-key SCP03_ENC_ISDR:000102030405060708090a0b0c0d0e0f`
|
||||
|
||||
In order to avoid having to repeat the column key for each and every
|
||||
column of a group of keys within a keyset, there are pre-defined column
|
||||
group aliases, which will make sure that the specified key will be used
|
||||
by all columns of the set:
|
||||
|
||||
* `UICC_SCP02` is a group alias for `UICC_SCP02_KIC1`, `UICC_SCP02_KID1`, `UICC_SCP02_KIK1`
|
||||
* `UICC_SCP03` is a group alias for `UICC_SCP03_KIC1`, `UICC_SCP03_KID1`, `UICC_SCP03_KIK1`
|
||||
* `SCP03_ECASD` is a group alias for `SCP03_ENC_ECASD`, `SCP03_MAC_ECASD`, `SCP03_DEK_ECASD`
|
||||
* `SCP03_ISDA` is a group alias for `SCP03_ENC_ISDA`, `SCP03_MAC_ISDA`, `SCP03_DEK_ISDA`
|
||||
* `SCP03_ISDR` is a group alias for `SCP03_ENC_ISDR`, `SCP03_MAC_ISDR`, `SCP03_DEK_ISDR`
|
||||
|
||||
|
||||
Field naming
|
||||
------------
|
||||
|
||||
* For look-up of UICC/SIM/USIM/ISIM or eSIM profile specific key
|
||||
material, pySim uses the `ICCID` field as lookup key.
|
||||
|
||||
* For look-up of eUICC specific key material (like SCP03 keys for the
|
||||
ISD-R, ECASD), pySim uses the `EID` field as lookup key.
|
||||
|
||||
As soon as the CardKeyProviderCsv finds a line (row) in your CSV where
|
||||
the ICCID or EID match, it looks for the column containing the requested
|
||||
data.
|
||||
|
||||
|
||||
ADM PIN
|
||||
~~~~~~~
|
||||
|
||||
The `verify_adm` command will attempt to look up the `ADM1` column
|
||||
indexed by the ICCID of the SIM/UICC.
|
||||
|
||||
|
||||
SCP02 / SCP03
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
SCP02 and SCP03 each use key triplets consisting if ENC, MAC and DEK
|
||||
keys. For more details, see the applicable GlobalPlatform
|
||||
specifications.
|
||||
|
||||
If you do not want to manually enter the key material for each specific
|
||||
card as arguments to the `establish_scp02` or `establish_scp03`
|
||||
commands, you can make use of the `--key-provider-suffix` option. pySim
|
||||
uses this suffix to compose the column names for the CardKeyProvider as
|
||||
follows.
|
||||
|
||||
* `SCP02_ENC_` + suffix for the SCP02 ciphering key
|
||||
* `SCP02_MAC_` + suffix for the SCP02 MAC key
|
||||
* `SCP02_DEK_` + suffix for the SCP02 DEK key
|
||||
* `SCP03_ENC_` + suffix for the SCP03 ciphering key
|
||||
* `SCP03_MAC_` + suffix for the SCP03 MAC key
|
||||
* `SCP03_DEK_` + suffix for the SCP03 DEK key
|
||||
|
||||
So for example, if you are using a command like `establish_scp03
|
||||
--key-provider-suffix ISDR`, then the column names for the key material
|
||||
look-up are `SCP03_ENC_ISDR`, `SCP03_MAC_ISDR` and `SCP03_DEK_ISDR`,
|
||||
respectively.
|
||||
|
||||
The identifier used for look-up is determined by the definition of the
|
||||
Security Domain. For example, the eUICC ISD-R and ECASD will use the EID
|
||||
of the eUICC. On the other hand, the ISD-P of an eSIM or the ISD of an
|
||||
UICC will use the ICCID.
|
||||
@@ -41,14 +41,8 @@ pySim consists of several parts:
|
||||
shell
|
||||
trace
|
||||
legacy
|
||||
smpp2sim
|
||||
library
|
||||
library-esim
|
||||
osmo-smdpp
|
||||
sim-rest
|
||||
suci-keytool
|
||||
saip-tool
|
||||
wsrc
|
||||
|
||||
|
||||
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
|
||||
existed long before ``pySim-shell``.
|
||||
|
||||
These days, it is highly recommended to use ``pySim-shell`` instead of these
|
||||
These days, you should primarily use ``pySim-shell`` instead of these
|
||||
legacy tools.
|
||||
|
||||
pySim-prog
|
||||
----------
|
||||
|
||||
``pySim-prog`` was the first part of the pySim software suite. It started as a
|
||||
tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and was
|
||||
later extended to a variety of other cards. As the number of features supported
|
||||
became no longer bearable to express with command-line arguments, `pySim-shell`
|
||||
was created.
|
||||
``pySim-prog`` was the first part of the pySim software suite. It started as
|
||||
a tool to write ICCID, IMSI, MSISDN and Ki to very simplistic SIM cards, and
|
||||
was later extended to a variety of other cards. As the number of features supported
|
||||
became no longer bearable to express with command-line arguments, `pySim-shell` was
|
||||
created.
|
||||
|
||||
Basic use cases can still use `pySim-prog`.
|
||||
|
||||
@@ -22,180 +22,36 @@ Program customizable SIMs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Two modes are possible:
|
||||
|
||||
- one where the user specifies every parameter manually:
|
||||
- one where you specify every parameter manually :
|
||||
|
||||
This is the most common way to use ``pySim-prog``. The user will specify all relevant parameters directly via the
|
||||
commandline. A typical commandline would look like this:
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --ki <ki_value> --opc <opc_value> --mcc <mcc_value> --mnc <mnc_value>
|
||||
--country <country_code> --imsi <imsi_value> --iccid <iccid_value> --pin-adm <adm_pin>``
|
||||
|
||||
Please note, that this already lengthy commandline still only contains the most common card parameters. For a full
|
||||
list of all possible parameters, use the ``--help`` option of ``pySim-prog``. It is also important to mention
|
||||
that not all parameters are supported by all card types. In particular, very simple programmable SIM cards will only
|
||||
support a very basic set of parameters, such as MCC, MNC, IMSI and KI values.
|
||||
|
||||
- one where the parameters are generated from a minimal set:
|
||||
|
||||
It is also possible to leave the generation of certain parameters to ``pySim-prog``. This is in particular helpful
|
||||
when a large number of cards should be initialized with randomly generated key material.
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --mcc <mcc_value> --mnc <mnc_value> --secret <random_secret> --num <card_number> --pin-adm <adm_pin>``
|
||||
|
||||
The parameter ``--secret`` specifies a random seed that is used to generate the card individual parameters. (IMSI).
|
||||
The secret should contain enough randomness to avoid conflicts. It is also recommended to store the secret safely,
|
||||
in case cards have to be re-generated or the current card batch has to be extended later. For security reasons, the
|
||||
key material, which is also card individual, will not be derived from the random seed. Instead a new random set of
|
||||
Ki and OPc will be generated during each programming cycle. This means fresh keys are generated, even when the
|
||||
``--num`` remains unchanged.
|
||||
|
||||
The parameter ``--num`` specifies a card individual number. This number will be manged into the random seed so that
|
||||
it serves as an identifier for a particular set of randomly generated parameters.
|
||||
|
||||
In the example above the parameters ``--mcc``, and ``--mnc`` are specified as well, since they identify the GSM
|
||||
network where the cards should operate in, it is absolutely required to keep them static. ``pySim-prog`` will use
|
||||
those parameters to generate a valid IMSI that thas the specified MCC/MNC at the beginning and a random tail.
|
||||
|
||||
Specifying the card type:
|
||||
|
||||
``pySim-prog`` usually autodetects the card type. In case auto detection does not work, it is possible to specify
|
||||
the parameter ``--type``. The following card types are supported:
|
||||
|
||||
* Fairwaves-SIM
|
||||
* fakemagicsim
|
||||
* gialersim
|
||||
* grcardsim
|
||||
* magicsim
|
||||
* OpenCells-SIM
|
||||
* supersim
|
||||
* sysmoISIM-SJA2
|
||||
* sysmoISIM-SJA5
|
||||
* sysmosim-gr1
|
||||
* sysmoSIM-GR2
|
||||
* sysmoUSIM-SJS1
|
||||
* Wavemobile-SIM
|
||||
|
||||
Specifying the card reader:
|
||||
|
||||
It is most common to use ``pySim-prog`` together whith a PCSC reader. The PCSC reader number is specified via the
|
||||
``--pcsc-device`` or ``-p`` option. However, other reader types (such as serial readers and modems) are supported. Use
|
||||
the ``--help`` option of ``pySim-prog`` for more information.
|
||||
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -i <IMSI> -s <ICCID>``
|
||||
|
||||
|
||||
Card programming using CSV files
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- one where they are generated from some minimal set :
|
||||
|
||||
To simplify the card programming process, ``pySim-prog`` also allows to read
|
||||
the card parameters from a CSV file. When a CSV file is used as input, the
|
||||
user does not have to craft an individual commandline for each card. Instead
|
||||
all card related parameters are automatically drawn from the CSV file.
|
||||
``./pySim-prog.py -n 26C3 -c 49 -x 262 -y 42 -z <random_string_of_choice> -j <card_num>``
|
||||
|
||||
A CSV files may hold rows for multiple (hundreds or even thousands) of
|
||||
cards. ``pySim-prog`` is able to identify the rows either by ICCID
|
||||
(recommended as ICCIDs are normally not changed) or IMSI.
|
||||
With <random_string_of_choice> and <card_num>, the soft will generate
|
||||
'predictable' IMSI and ICCID, so make sure you choose them so as not to
|
||||
conflict with anyone. (for eg. your name as <random_string_of_choice> and
|
||||
0 1 2 ... for <card num>).
|
||||
|
||||
The CSV file format is a flexible format with mandatory and optional columns,
|
||||
here the same rules as for the commandline parameters apply. The column names
|
||||
match the command line options. The CSV file may also contain columns that are
|
||||
unknown to pySim-prog, such as inventory numbers, nicknames or parameters that
|
||||
are unrelated to the card programming process. ``pySim-prog`` will silently
|
||||
ignore all unknown columns.
|
||||
|
||||
A CSV file may contain the following columns:
|
||||
|
||||
* name
|
||||
* iccid (typically used as key)
|
||||
* mcc
|
||||
* mnc
|
||||
* imsi (may be used as key, but not recommended)
|
||||
* smsp
|
||||
* ki
|
||||
* opc
|
||||
* acc
|
||||
* pin_adm, adm1 or pin_adm_hex (must be present)
|
||||
* msisdn
|
||||
* epdgid
|
||||
* epdgSelection
|
||||
* pcscf
|
||||
* ims_hdomain
|
||||
* impi
|
||||
* impu
|
||||
* opmode
|
||||
* fplmn
|
||||
|
||||
Due to historical reasons, and to maintain the compatibility between multiple different CSV file formats, the ADM pin
|
||||
may be stored in three different columns. Only one of the three columns must be available.
|
||||
|
||||
* adm1: This column contains the ADM pin in numeric ASCII digit format. This format is the most common.
|
||||
* pin_adm: Same as adm1, only the column name is different
|
||||
* pin_adm_hex: If the ADM pin consists of raw HEX digits, rather then of numerical ASCII digits, then the ADM pin
|
||||
can also be provided as HEX string using this column.
|
||||
|
||||
The following example shows a typical minimal example
|
||||
::
|
||||
|
||||
"imsi","iccid","acc","ki","opc","adm1"
|
||||
"999700000053010","8988211000000530108","0001","51ACE8BD6313C230F0BFE1A458928DF0","E5A00E8DE427E21B206526B5D1B902DF","65942330"
|
||||
"999700000053011","8988211000000530116","0002","746AAFD7F13CFED3AE626B770E53E860","38F7CE8322D2A7417E0BBD1D7B1190EC","13445792"
|
||||
"999700123053012","8988211000000530124","0004","D0DA4B7B150026ADC966DC637B26429C","144FD3AEAC208DFFF4E2140859BAE8EC","53540383"
|
||||
"999700000053013","8988211000000530132","0008","52E59240ABAC6F53FF5778715C5CE70E","D9C988550DC70B95F40342298EB84C5E","26151368"
|
||||
"999700000053014","8988211000000530140","0010","3B4B83CB9C5F3A0B41EBD17E7D96F324","D61DCC160E3B91F284979552CC5B4D9F","64088605"
|
||||
"999700000053015","8988211000000530157","0020","D673DAB320D81039B025263610C2BBB3","4BCE1458936B338067989A06E5327139","94108841"
|
||||
"999700000053016","8988211000000530165","0040","89DE5ACB76E06D14B0F5D5CD3594E2B1","411C4B8273FD7607E1885E59F0831906","55184287"
|
||||
"999700000053017","8988211000000530173","0080","977852F7CEE83233F02E69E211626DE1","2EC35D48DBF2A99C07D4361F19EF338F","70284674"
|
||||
|
||||
The following commandline will instruct ``pySim-prog`` to use the provided CSV file as parameter source and the
|
||||
ICCID (read from the card before programming) as a key to identify the card. To use the IMSI as a key, the parameter
|
||||
``--read-imsi`` can be used instead of ``--read-iccid``. However, this option is only recommended to be used in very
|
||||
specific corner cases.
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_csv_file> --source csv --read-iccid``
|
||||
|
||||
It is also possible to pick a row from the CSV file by manually providing an ICCID (option ``--iccid``) or an IMSI
|
||||
(option ``--imsi``) that is then used as a key to find the matching row in the CSV file.
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_csv_file> --source csv --iccid <iccid_value>``
|
||||
|
||||
|
||||
Writing CSV files
|
||||
~~~~~~~~~~~~~~~~~
|
||||
``pySim-prog`` is also able to generate CSV files that contain a subset of the parameters it has generated or received
|
||||
from some other source (commandline, CSV-File). The generated file will be header-less and contain the following
|
||||
columns:
|
||||
|
||||
* name
|
||||
* iccid
|
||||
* mcc
|
||||
* mnc
|
||||
* imsi
|
||||
* smsp
|
||||
* ki
|
||||
* opc
|
||||
|
||||
A commandline that makes use of the CSV write feature would look like this:
|
||||
|
||||
``pySim-prog.py -p <pcsc_reader> --read-csv <path_to_input_csv_file> --read-iccid --source csv --write-csv <path_to_output_csv_file>``
|
||||
|
||||
|
||||
Batch programming
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
In case larger card batches need to be programmed, it is possible to use the ``--batch`` parameter to run ``pySim-prog`` in batch mode.
|
||||
|
||||
The batch mode will prompt the user to insert a card. Once a card is detected in the reader, the programming is carried out. The user may then remove the card again and the process starts over. This allows for a quick and efficient card programming without permanent commandline interaction.
|
||||
You also need to enter some parameters to select the device :
|
||||
-t TYPE : type of card (supersim, magicsim, fakemagicsim or try 'auto')
|
||||
-d DEV : Serial port device (default /dev/ttyUSB0)
|
||||
-b BAUD : Baudrate (default 9600)
|
||||
|
||||
|
||||
pySim-read
|
||||
----------
|
||||
|
||||
``pySim-read`` allows to read some of the most important data items from a SIM
|
||||
card. This means it will only read some files of the card, and will only read
|
||||
files accessible to a normal user (without any special authentication)
|
||||
``pySim-read`` allows you to read some data from a SIM card. It will only some files
|
||||
of the card, and will only read files accessible to a normal user (without any special authentication)
|
||||
|
||||
These days, it is recommended to use the ``export`` command of ``pySim-shell``
|
||||
instead. It performs a much more comprehensive export of all of the [standard]
|
||||
files that can be found on the card. To get a human-readable decode instead of
|
||||
the raw hex export, you can use ``export --json``.
|
||||
These days, you should use the ``export`` command of ``pySim-shell``
|
||||
instead. It performs a much more comprehensive export of all of the
|
||||
[standard] files that can be found on the card. To get a human-readable
|
||||
decode instead of the raw hex export, you can use ``export --json``.
|
||||
|
||||
Specifically, pySim-read will dump the following:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
pySim construct utilities
|
||||
-------------------------
|
||||
|
||||
.. automodule:: pySim.construct
|
||||
:members:
|
||||
|
||||
pySim TLV utilities
|
||||
-------------------
|
||||
|
||||
.. automodule:: pySim.tlv
|
||||
:members:
|
||||
|
||||
pySim utility functions
|
||||
-----------------------
|
||||
|
||||
|
||||
@@ -19,21 +19,13 @@ support for profile personalization yet.
|
||||
|
||||
osmo-smdpp currently
|
||||
|
||||
* [by default] uses test certificates copied from GSMA SGP.26 into `./smdpp-data/certs`, assuming that your
|
||||
osmo-smdpp would be running at the host name `testsmdpplus1.example.com`. You can of course replace those
|
||||
certificates with your own, whether SGP.26 derived or part of a *private root CA* setup with mathcing eUICCs.
|
||||
* doesn't understand profile state. Any profile can always be downloaded any number of times, irrespective
|
||||
of the EID or whether it was donwloaded before. This is actually very useful for R&D and testing, as it
|
||||
doesn't require you to generate new profiles all the time. This logic of course is unsuitable for
|
||||
production usage.
|
||||
* doesn't perform any personalization, so the IMSI/ICCID etc. are always identical (the ones that are stored in
|
||||
the respective UPP `.der` files)
|
||||
* always provides the exact same profile to every request. The profile always has the same IMSI and
|
||||
ICCID.
|
||||
* **is absolutely insecure**, as it
|
||||
|
||||
* does not perform all of the mandatory certificate verification (it checks the certificate chain, but not
|
||||
the expiration dates nor any CRL)
|
||||
* does not evaluate/consider any *Confirmation Code*
|
||||
* stores the sessions in an unencrypted *python shelve* and is hence leaking one-time key materials
|
||||
* does not perform any certificate verification
|
||||
* does not evaluate/consider any *Matching ID* or *Confirmation Code*
|
||||
* stores the sessions in an unencrypted _python shelve_ and is hence leaking one-time key materials
|
||||
used for profile encryption and signing.
|
||||
|
||||
|
||||
@@ -79,33 +71,18 @@ If you use `nginx` as web server, you can use the following configuration snippe
|
||||
You can of course achieve a similar functionality with apache, lighttpd or many other web server
|
||||
software.
|
||||
|
||||
supplementary files
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The `smdpp-data/certs` directory contains the DPtls, DPauth and DPpb as well as CI certificates
|
||||
used; they are copied from GSMA SGP.26 v2. You can of course replace them with custom certificates
|
||||
if you're operating eSIM with a *private root CA*.
|
||||
osmo-smdpp
|
||||
~~~~~~~~~~
|
||||
|
||||
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used. The file names (without
|
||||
.der suffix) are looked up by the matchingID parameter from the activation code presented by the LPA.
|
||||
osmo-smdpp currently doesn't have any configuration file or command line options. You just run it,
|
||||
and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
|
||||
|
||||
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 it without any arguments, and it will bind its plain-HTTP ES9+ interface to
|
||||
`localhost` TCP port 8000.
|
||||
The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) used.
|
||||
|
||||
osmo-smdpp currently doesn't have any configuration file.
|
||||
|
||||
There are command line options for binding:
|
||||
|
||||
Bind the HTTP ES9+ to a port other than 8000::
|
||||
|
||||
./osmo-smdpp.py -p 8001
|
||||
|
||||
Bind the HTTP ES9+ to a different local interface::
|
||||
|
||||
./osmo-smdpp.py -H 127.0.0.1
|
||||
|
||||
DNS setup for your LPA
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -114,20 +91,3 @@ The LPA must resolve `testsmdpplus1.example.com` to the IP address of your TLS p
|
||||
|
||||
It must also accept the TLS certificates used by your TLS proxy.
|
||||
|
||||
Supported eUICC
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
If you run osmo-smdpp with the included SGP.26 certificates, you must use an eUICC with matching SGP.26
|
||||
certificates, i.e. the EUM certificate must be signed by a SGP.26 test root CA and the eUICC certificate
|
||||
in turn must be signed by that SGP.26 EUM certificate.
|
||||
|
||||
sysmocom (sponsoring development and maintenance of pySim and osmo-smdpp) is selling SGP.26 test eUICC
|
||||
as `sysmoEUICC1-C2T`. They are publicly sold in the `sysmocom webshop <https://shop.sysmocom.de/eUICC-for-consumer-eSIM-RSP-with-SGP.26-Test-Certificates/sysmoEUICC1-C2T>`_.
|
||||
|
||||
In general you can use osmo-smdpp also with certificates signed by any other certificate authority. You
|
||||
just always must ensure that the certificates of the SM-DP+ are signed by the same root CA as those of your
|
||||
eUICCs.
|
||||
|
||||
Hypothetically, osmo-smdpp could also be operated with GSMA production certificates, but it would require
|
||||
that somebody brings the code in-line with all the GSMA security requirements (HSM support, ...) and operate
|
||||
it in a GSMA SAS-SM accredited environment and pays for the related audits.
|
||||
|
||||
@@ -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,
|
||||
@@ -20,9 +20,6 @@ The pySim-shell interactive shell provides commands for
|
||||
|
||||
* if your card supports it, and you have the related privileges: resizing, creating, enabling and disabling of
|
||||
files
|
||||
* performing GlobalPlatform operations, including establishment of Secure Channel Protocol (SCP), Installing
|
||||
applications, installing key material, etc.
|
||||
* listing/enabling/disabling/deleting eSIM profiles on Consumer eUICC
|
||||
|
||||
By means of using the python ``cmd2`` module, various useful features improve usability:
|
||||
|
||||
@@ -67,18 +64,8 @@ Usage Examples
|
||||
:caption: Tutorials for pySIM-shell:
|
||||
|
||||
suci-tutorial
|
||||
cap-tutorial
|
||||
|
||||
|
||||
Advanced Topics
|
||||
---------------
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Advanced pySIM-shell topics
|
||||
|
||||
card-key-provider
|
||||
remote-access
|
||||
|
||||
cmd2 basics
|
||||
-----------
|
||||
|
||||
@@ -146,32 +133,6 @@ optional files in some later 3GPP release) were not found on the card, or were i
|
||||
trying to SELECT them.
|
||||
|
||||
|
||||
fsdump
|
||||
~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim-shell
|
||||
:func: PySimCommands.fsdump_parser
|
||||
|
||||
Please note that `fsdump` works relative to the current working
|
||||
directory, so if you are in `MF`, then the dump will contain all known
|
||||
files on the card. However, if you are in `ADF.ISIM`, only files below
|
||||
that ADF will be part of the dump.
|
||||
|
||||
Furthermore, it is strongly advised to first enter the ADM1 pin
|
||||
(`verify_adm`) to maximize the chance of having permission to read
|
||||
all/most files.
|
||||
|
||||
One use case for this is to systematically analyze the differences between the contents of two
|
||||
cards. To do this, you can create fsdumps of the two cards, and then use some general-purpose JSON
|
||||
diffing tool like `jycm --show` (see https://github.com/eggachecat/jycm).
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
pySIM-shell (00:MF)> fsdump > /tmp/fsdump.json
|
||||
pySIM-shell (00:MF)>
|
||||
|
||||
|
||||
tree
|
||||
~~~~
|
||||
Display a tree of the card filesystem. It is important to note that this displays a tree
|
||||
@@ -439,19 +400,7 @@ verify_chv
|
||||
|
||||
deactivate_file
|
||||
~~~~~~~~~~~~~~~
|
||||
Deactivate the currently selected file. A deactivated file can no longer be accessed
|
||||
for any further operation (such as selecting and subsequently reading or writing).
|
||||
|
||||
Any access to a file that is deactivated will trigger the error
|
||||
*SW 6283 'Selected file invalidated/disabled'*
|
||||
|
||||
In order to re-access a deactivated file, you need to activate it again, see the
|
||||
`activate_file` command below. Note that for *deactivation* the to-be-deactivated
|
||||
EF must be selected, but for *activation*, the DF above the to-be-activated
|
||||
EF must be selected!
|
||||
|
||||
This command sends a DEACTIVATE FILE APDU to
|
||||
the card (used to be called INVALIDATE in TS 11.11 for classic SIM).
|
||||
Deactivate the currently selected file. This used to be called INVALIDATE in TS 11.11.
|
||||
|
||||
|
||||
activate_file
|
||||
@@ -512,18 +461,7 @@ sequence including the electrical power down.
|
||||
:module: pySim.ts_102_221
|
||||
:func: CardProfileUICC.AddlShellCommands.resume_uicc_parser
|
||||
|
||||
terminal_capability
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
This command allows you to perform the TERMINAL CAPABILITY command towards the card.
|
||||
|
||||
TS 102 221 specifies the TERMINAL CAPABILITY command using which the
|
||||
terminal (Software + hardware talking to the card) can expose their
|
||||
capabilities. This is also used in the eUICC universe to let the eUICC
|
||||
know which features are supported.
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.ts_102_221
|
||||
:func: CardProfileUICC.AddlShellCommands.term_cap_parser
|
||||
|
||||
|
||||
Linear Fixed EF commands
|
||||
@@ -544,9 +482,6 @@ read_record_decoded
|
||||
:module: pySim.filesystem
|
||||
:func: LinFixedEF.ShellCommands.read_rec_dec_parser
|
||||
|
||||
If this command fails, it means that the record is not decodable, and you should use the :ref:`read_record`
|
||||
command and proceed with manual decoding of the contents.
|
||||
|
||||
|
||||
read_records
|
||||
~~~~~~~~~~~~
|
||||
@@ -561,9 +496,6 @@ read_records_decoded
|
||||
:module: pySim.filesystem
|
||||
:func: LinFixedEF.ShellCommands.read_recs_dec_parser
|
||||
|
||||
If this command fails, it means that the record[s] are not decodable, and you should use the :ref:`read_records`
|
||||
command and proceed with manual decoding of the contents.
|
||||
|
||||
|
||||
update_record
|
||||
~~~~~~~~~~~~~
|
||||
@@ -578,9 +510,6 @@ update_record_decoded
|
||||
:module: pySim.filesystem
|
||||
:func: LinFixedEF.ShellCommands.upd_rec_dec_parser
|
||||
|
||||
If this command fails, it means that the record is not encodable; please check your input and/or use the raw
|
||||
:ref:`update_record` command.
|
||||
|
||||
|
||||
edit_record_decoded
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -599,12 +528,6 @@ back to the record on the SIM card.
|
||||
|
||||
This allows for easy interactive modification of records.
|
||||
|
||||
If this command fails before the editor is spawned, it means that the current record contents is not decodable,
|
||||
and you should use the :ref:`update_record_decoded` or :ref:`update_record` command.
|
||||
|
||||
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
|
||||
encodable; please check your input and/or us the raw :ref:`update_record` comamdn.
|
||||
|
||||
|
||||
decode_hex
|
||||
~~~~~~~~~~
|
||||
@@ -633,8 +556,6 @@ read_binary_decoded
|
||||
:module: pySim.filesystem
|
||||
:func: TransparentEF.ShellCommands.read_bin_dec_parser
|
||||
|
||||
If this command fails, it means that the file is not decodable, and you should use the :ref:`read_binary`
|
||||
command and proceed with manual decoding of the contents.
|
||||
|
||||
update_binary
|
||||
~~~~~~~~~~~~~
|
||||
@@ -688,10 +609,6 @@ The below example demonstrates this by modifying the ciphering indicator field w
|
||||
"extensions": "ff"
|
||||
}
|
||||
|
||||
If this command fails, it means that the file is not encodable; please check your input and/or use the raw
|
||||
:ref:`update_binary` command.
|
||||
|
||||
|
||||
edit_binary_decoded
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
This command will read the selected binary EF, decode it to its JSON representation, save
|
||||
@@ -705,12 +622,6 @@ to the SIM card.
|
||||
|
||||
This allows for easy interactive modification of file contents.
|
||||
|
||||
If this command fails before the editor is spawned, it means that the current file contents is not decodable,
|
||||
and you should use the :ref:`update_binary_decoded` or :ref:`update_binary` command.
|
||||
|
||||
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
|
||||
encodable; please check your input and/or us the raw :ref:`update_binary` comamdn.
|
||||
|
||||
|
||||
decode_hex
|
||||
~~~~~~~~~~
|
||||
@@ -1003,25 +914,7 @@ aram_delete_all
|
||||
~~~~~~~~~~~~~~~
|
||||
This command will request deletion of all access rules stored within the
|
||||
ARA-M applet. Use it with caution, there is no undo. Any rules later
|
||||
intended must be manually inserted again using :ref:`aram_store_ref_ar_do`
|
||||
|
||||
|
||||
aram_lock
|
||||
~~~~~~~~~
|
||||
This command allows to lock the access to the STORE DATA command. This renders
|
||||
all access rules stored within the ARA-M applet effectively read-only. The lock
|
||||
can only be removed via a secure channel to the security domain and is therefore
|
||||
suitable to prevent unauthorized changes to ARA-M rules.
|
||||
|
||||
Removal of the lock:
|
||||
::
|
||||
|
||||
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> install_for_personalization A00000015141434C00
|
||||
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> apdu --expect-sw 9000 80E2900001A2
|
||||
|
||||
NOTE: ARA-M Locking is a proprietary feature that is specific to sysmocom's
|
||||
fork of Bertrand Martel's ARA-M implementation. ARA-M Locking is supported in
|
||||
newer (2025) applet versions from v0.1.0 onward.
|
||||
intended must be manually inserted again using `aram_store_ref_ar_do`
|
||||
|
||||
|
||||
GlobalPlatform commands
|
||||
@@ -1036,88 +929,6 @@ get_data
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.get_data_parser
|
||||
|
||||
get_status
|
||||
~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.get_status_parser
|
||||
|
||||
set_status
|
||||
~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.set_status_parser
|
||||
|
||||
store_data
|
||||
~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.store_data_parser
|
||||
|
||||
put_key
|
||||
~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.put_key_parser
|
||||
|
||||
delete_key
|
||||
~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.del_key_parser
|
||||
|
||||
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
|
||||
--------------------
|
||||
@@ -1155,7 +966,7 @@ es10x_store_data
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.es10x_store_data_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.es10x_store_data_parser
|
||||
|
||||
get_euicc_configured_addresses
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1174,7 +985,7 @@ set_default_dp_address
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.set_def_dp_addr_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.set_def_dp_addr_parser
|
||||
|
||||
get_euicc_challenge
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1317,7 +1128,7 @@ remove_notification_from_list
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.rem_notif_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.rem_notif_parser
|
||||
|
||||
Example::
|
||||
|
||||
@@ -1366,7 +1177,7 @@ enable_profile
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.en_prof_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.en_prof_parser
|
||||
|
||||
Example (successful)::
|
||||
|
||||
@@ -1388,7 +1199,7 @@ disable_profile
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.dis_prof_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.dis_prof_parser
|
||||
|
||||
Example (successful)::
|
||||
|
||||
@@ -1402,7 +1213,7 @@ delete_profile
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.del_prof_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.del_prof_parser
|
||||
|
||||
Example::
|
||||
|
||||
@@ -1411,13 +1222,6 @@ Example::
|
||||
"delete_result": "ok"
|
||||
}
|
||||
|
||||
euicc_memory_reset
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.mem_res_parser
|
||||
|
||||
|
||||
get_eid
|
||||
~~~~~~~
|
||||
@@ -1436,7 +1240,7 @@ set_nickname
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.set_nickname_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.set_nickname_parser
|
||||
|
||||
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 (succcess):
|
||||
::
|
||||
|
||||
{
|
||||
"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 (int 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
|
||||
=======================
|
||||
========================
|
||||
|
||||
SUPI/SUCI Concealment is a feature of 5G-Standalone (SA) to encrypt the
|
||||
IMSI/SUPI with a network operator public key. 3GPP Specifies two different
|
||||
variants for this:
|
||||
|
||||
* SUCI calculation *in the UE*, using key data from the SIM
|
||||
* SUCI calculation *in the UE*, using data from the SIM
|
||||
* SUCI calculation *on the card itself*
|
||||
|
||||
pySim supports writing the 5G-specific files for *SUCI calculation in the UE* on USIM cards, assuming
|
||||
that your cards contain the required files, and you have the privileges/credentials to write to them.
|
||||
This is the case using sysmocom sysmoISIM-SJA2 or any flavor of sysmoISIM-SJA5.
|
||||
pySIM supports writing the 5G-specific files for *SUCI calculation in the UE* on USIM cards, assuming that
|
||||
your cards contain the required files, and you have the privileges/credentials to write to them. This is
|
||||
the case using sysmocom sysmoISIM-SJA2 cards (or successor products).
|
||||
|
||||
There is no 3GPP/ETSI standard method for configuring *SUCI calculation on the card*; pySim currently
|
||||
supports the vendor-specific method for the sysmoISIM-SJA5-S17).
|
||||
In short, you can enable SUCI with these steps:
|
||||
|
||||
This document describes both methods.
|
||||
* activate USIM **Service 124**
|
||||
* make sure USIM **Service 125** is disabled
|
||||
* store the public keys in **SUCI_Calc_Info**
|
||||
* set the **Routing Indicator** (required)
|
||||
|
||||
If you want to disable the feature, you can just disable USIM Service 124 (and 125).
|
||||
|
||||
Technical References
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This guide covers the basic workflow of provisioning SIM cards with the 5G SUCI feature. For detailed information on the SUCI feature and file contents, the following documents are helpful:
|
||||
|
||||
* USIM files and structure: `3GPP TS 31.102 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131102/16.06.00_60/ts_131102v160600p.pdf>`__
|
||||
* USIM tests (incl. file content examples): `3GPP TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__
|
||||
* Test keys for SUCI calculation: `3GPP TS 33.501 <https://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.pdf>`__
|
||||
* USIM files and structure: `TS 31.102 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131102/16.06.00_60/ts_131102v160600p.pdf>`__
|
||||
* USIM tests (incl. file content examples) `TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__
|
||||
|
||||
For specific information on sysmocom SIM cards, refer to
|
||||
|
||||
* the `sysmoISIM-SJA5 User Manual <https://sysmocom.de/manuals/sysmoisim-sja5-manual.pdf>`__ for the curent
|
||||
sysmoISIM-SJA5 product
|
||||
* the `sysmoISIM-SJA2 User Manual <https://sysmocom.de/manuals/sysmousim-manual.pdf>`__ for the older
|
||||
sysmoISIM-SJA2 product
|
||||
For specific information on sysmocom SIM cards, refer to Section 9.1 of the `sysmoUSIM User
|
||||
Manual <https://www.sysmocom.de/manuals/sysmousim-manual.pdf>`__.
|
||||
|
||||
--------------
|
||||
|
||||
|
||||
Enabling 5G SUCI *calculated in the UE*
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In short, you can enable *SUCI calculation in the UE* with these steps:
|
||||
|
||||
* activate USIM **Service 124**
|
||||
* make sure USIM **Service 125** is disabled
|
||||
* store the public keys in **EF.SUCI_Calc_Info**
|
||||
* set the **Routing Indicator** (required)
|
||||
|
||||
If you want to disable the feature, you can just disable USIM Service 124 (and 125) in `EF.UST`.
|
||||
|
||||
|
||||
Admin PIN
|
||||
---------
|
||||
|
||||
@@ -99,8 +83,8 @@ By default, the file is present but empty:
|
||||
missing Protection Scheme Identifier List data object tag
|
||||
9000: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -> {}
|
||||
|
||||
The following JSON config defines the testfile from 3GPP TS 31.121, Section 4.9.4 with
|
||||
test keys from 3GPP TS 33.501, Annex C.4. Highest priority (``0``) has a
|
||||
The following JSON config defines the testfile from `TS 31.121 <https://www.etsi.org/deliver/etsi_ts/131100_131199/131121/16.01.00_60/ts_131121v160100p.pdf>`__ Section 4.9.4 with
|
||||
test keys from `TS 33.501 <hhttps://www.etsi.org/deliver/etsi_ts/133500_133599/133501/16.05.00_60/ts_133501v160500p.pdf>`__ Annex C.4. Highest priority (``0``) has a
|
||||
Profile-B (``identifier: 2``) key in key slot ``1``, which means the key
|
||||
with ``hnet_pubkey_identifier: 27``.
|
||||
|
||||
@@ -113,7 +97,7 @@ with ``hnet_pubkey_identifier: 27``.
|
||||
{"priority": 2, "identifier": 0, "key_index": 0}],
|
||||
"hnet_pubkey_list": [
|
||||
{"hnet_pubkey_identifier": 27,
|
||||
"hnet_pubkey": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"},
|
||||
"hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"},
|
||||
{"hnet_pubkey_identifier": 30,
|
||||
"hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]
|
||||
}
|
||||
@@ -122,7 +106,7 @@ Write the config to file (must be single-line input as for now):
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> update_binary_decoded '{ "prot_scheme_id_list": [ {"priority": 0, "identifier": 2, "key_index": 1}, {"priority": 1, "identifier": 1, "key_index": 2}, {"priority": 2, "identifier": 0, "key_index": 0}], "hnet_pubkey_list": [ {"hnet_pubkey_identifier": 27, "hnet_pubkey": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"}, {"hnet_pubkey_identifier": 30, "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]}'
|
||||
pySIM-shell (00:MF/ADF.USIM/DF.5GS/EF.SUCI_Calc_Info)> update_binary_decoded '{ "prot_scheme_id_list": [ {"priority": 0, "identifier": 2, "key_index": 1}, {"priority": 1, "identifier": 1, "key_index": 2}, {"priority": 2, "identifier": 0, "key_index": 0}], "hnet_pubkey_list": [ {"hnet_pubkey_identifier": 27, "hnet_pubkey": "0272DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD1"}, {"hnet_pubkey_identifier": 30, "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"}]}'
|
||||
|
||||
WARNING: These are TEST KEYS with publicly known/specified private keys, and hence unsafe for live/secure
|
||||
deployments! For use in production networks, you need to generate your own set[s] of keys.
|
||||
@@ -166,7 +150,7 @@ First, check out the USIM Service Table (UST):
|
||||
pySIM-shell (00:MF/ADF.USIM/EF.UST)> read_binary_decoded
|
||||
9000: beff9f9de73e0408400170730000002e00000000 -> [2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 25, 27, 28, 29, 33, 34, 35, 38, 39, 42, 43, 44, 45, 46, 51, 60, 71, 73, 85, 86, 87, 89, 90, 93, 94, 95, 122, 123, 124, 126]
|
||||
|
||||
.. list-table:: From 3GPP TS 31.102
|
||||
.. list-table:: From TS31.102
|
||||
:widths: 15 40
|
||||
:header-rows: 1
|
||||
|
||||
@@ -200,7 +184,7 @@ be disabled.
|
||||
USIM Error with 5G and sysmoISIM
|
||||
--------------------------------
|
||||
|
||||
sysmoISIM-SJA2 come 5GS-enabled. By default however, the configuration stored
|
||||
sysmoISIMs come 5GS-enabled. By default however, the configuration stored
|
||||
in the card file-system is **not valid** for 5G networks: Service 124 is enabled,
|
||||
but EF.SUCI_Calc_Info and EF.Routing_Indicator are empty files (hence
|
||||
do not contain valid data).
|
||||
@@ -209,62 +193,3 @@ At least for 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
|
||||
smartphone refuses to connect to any 5G network, try to disable the UST
|
||||
service 124.
|
||||
|
||||
sysmoISIM-SJA5 are shipped with a more forgiving default, with valid EF.Routing_Indicator
|
||||
contents and disabled Service 124
|
||||
|
||||
|
||||
SUCI calculation by the USIM
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The SUCI calculation can also be performed by the USIM application on the UICC
|
||||
directly. The UE then uses the GET IDENTITY command (see also 3GPP TS 31.102,
|
||||
section 7.5) to retrieve a SUCI value.
|
||||
|
||||
The sysmoISIM-SJA5-S17 supports *SUCI calculation by the USIM*. The configuration
|
||||
is not much different to the above described configuration of *SUCI calculation
|
||||
in the UE*.
|
||||
|
||||
The main difference is how the key provisioning is done. When the SUCI
|
||||
calculation is done by the USIM, then the key material is not accessed by the
|
||||
UE. The specification (see also 3GPP TS 31.102, section 7.5.1.1), also does not
|
||||
specify any file or file format to store the key material. This means the exact
|
||||
way to perform the key provisioning is an implementation detail of the USIM
|
||||
card application.
|
||||
|
||||
In the case of sysmoISIM-SJA5-S17, the key material for *SUCI calculation by the USIM* is stored in
|
||||
`ADF.USIM/DF.SAIP/EF.SUCI_Calc_Info` (**not** in `ADF.USIM/DF.5GS/EF.SUCI_Calc_Info`!).
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (00:MF)> select MF
|
||||
pySIM-shell (00:MF)> select ADF.USIM
|
||||
pySIM-shell (00:MF/ADF.USIM)> select DF.SAIP
|
||||
pySIM-shell (00:MF/ADF.USIM/DF.SAIP)> select EF.SUCI_Calc_Info
|
||||
|
||||
The file format is exactly the same as specified in 3GPP TS 31.102, section
|
||||
4.4.11.8. This means the above described key provisioning procedure can be
|
||||
applied without any changes, except that the file location is different.
|
||||
|
||||
To signal to the UE that the USIM is setup up for SUCI calculation, service
|
||||
125 must be enabled in addition to service 124 (see also 3GPP TS 31.102,
|
||||
section 5.3.48)
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 124
|
||||
pySIM-shell (00:MF/ADF.USIM/EF.UST)> ust_service_activate 125
|
||||
|
||||
To verify that the SUCI calculation works as expected, it is possible to issue
|
||||
a GET IDENTITY command using pySim-shell:
|
||||
|
||||
::
|
||||
|
||||
select ADF.USIM
|
||||
get_identity
|
||||
|
||||
The USIM should then return a SUCI TLV Data object that looks like this:
|
||||
|
||||
::
|
||||
|
||||
SUCI TLV Data Object: 0199f90717ff021b027a2c58ce1c6b89df088a9eb4d242596dd75746bb5f3503d2cf58a7461e4fd106e205c86f76544e9d732226a4e1
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
WebSocket Remote Card (WSRC)
|
||||
============================
|
||||
|
||||
WSRC (*Web Socket Remote Card*) is a mechanism by which card readers can be made remotely available
|
||||
via a computer network. The transport mechanism is (as the name implies) a WebSocket. This transport
|
||||
method was chosen to be as firewall/NAT friendly as possible.
|
||||
|
||||
WSRC Network Architecture
|
||||
-------------------------
|
||||
|
||||
In a WSRC network, there are three major elements:
|
||||
|
||||
* The **WSRC Card Client** which exposes a locally attached smart card (usually via a Smart Card Reader)
|
||||
to a remote *WSRC Server*
|
||||
* The **WSRC Server** manges incoming connections from both *WSRC Card Clients* as well as *WSRC User Clients*
|
||||
* The **WSRC User Client** is a user application, like for example pySim-shell, which is accessing a remote
|
||||
card by connecting to the *WSRC Server* which relays the information to the selected *WSRC Card Client*
|
||||
|
||||
WSRC Protocol
|
||||
-------------
|
||||
|
||||
The WSRC protocl consits of JSON objects being sent over a websocket. The websocket communication itself
|
||||
is based on HTTP and should usually operate via TLS for security reasons.
|
||||
|
||||
The detailed protocol is currently still WIP. The plan is to document it here.
|
||||
|
||||
|
||||
pySim implementations
|
||||
---------------------
|
||||
|
||||
TBD
|
||||
403
osmo-smdpp.py
403
osmo-smdpp.py
@@ -32,13 +32,10 @@ from klein import Klein
|
||||
from twisted.web.iweb import IRequest
|
||||
import asn1tools
|
||||
|
||||
from osmocom.utils import h2b, b2h, swap_nibbles
|
||||
from pySim.utils import h2b, b2h, swap_nibbles
|
||||
|
||||
import pySim.esim.rsp as rsp
|
||||
from pySim.esim import saip, PMO
|
||||
from pySim.esim.es8p import *
|
||||
from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id
|
||||
from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError
|
||||
|
||||
# HACK: make this configurable
|
||||
DATA_DIR = './smdpp-data'
|
||||
@@ -73,12 +70,18 @@ def build_resp_header(js: dict, status: str = 'Executed-Success', status_code_da
|
||||
js['header']['functionExecutionStatus']['statusCodeData'] = status_code_data
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding, PublicFormat, PrivateFormat, NoEncryption
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography import x509
|
||||
|
||||
|
||||
def ecdsa_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')
|
||||
|
||||
def ecdsa_tr03111_to_dss(sig: bytes) -> bytes:
|
||||
"""convert an ECDSA signature from BSI TR-03111 format to DER: first get long integers; then encode those."""
|
||||
assert len(sig) == 64
|
||||
@@ -87,6 +90,52 @@ def ecdsa_tr03111_to_dss(sig: bytes) -> bytes:
|
||||
return encode_dss_signature(r, s)
|
||||
|
||||
|
||||
class CertAndPrivkey:
|
||||
"""A pair of certificate and private key, as used for ECDSA signing."""
|
||||
def __init__(self, required_policy_oid: Optional[x509.ObjectIdentifier] = None,
|
||||
cert: Optional[x509.Certificate] = None, priv_key = None):
|
||||
self.required_policy_oid = required_policy_oid
|
||||
self.cert = cert
|
||||
self.priv_key = priv_key
|
||||
|
||||
def cert_from_der_file(self, path: str):
|
||||
with open(path, 'rb') as f:
|
||||
cert = x509.load_der_x509_certificate(f.read())
|
||||
if self.required_policy_oid:
|
||||
# verify it is the right type of certificate (id-rspRole-dp-auth, id-rspRole-dp-auth-v2, etc.)
|
||||
assert cert_policy_has_oid(cert, self.required_policy_oid)
|
||||
self.cert = cert
|
||||
|
||||
def privkey_from_pem_file(self, path: str, password: Optional[str] = None):
|
||||
with open(path, 'rb') as f:
|
||||
self.priv_key = load_pem_private_key(f.read(), password)
|
||||
|
||||
def ecdsa_sign(self, plaintext: bytes) -> bytes:
|
||||
"""Sign some input-data using an ECDSA signature compliant with SGP.22,
|
||||
which internally refers to Global Platform 2.2 Annex E, which in turn points
|
||||
to BSI TS-03111 which states "concatengated raw R + S values". """
|
||||
sig = self.priv_key.sign(plaintext, ec.ECDSA(hashes.SHA256()))
|
||||
# convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes
|
||||
return ecdsa_dss_to_tr03111(sig)
|
||||
|
||||
def get_authority_key_identifier(self) -> x509.AuthorityKeyIdentifier:
|
||||
"""Return the AuthorityKeyIdentifier X.509 extension of the certificate."""
|
||||
return list(filter(lambda x: isinstance(x.value, x509.AuthorityKeyIdentifier), self.cert.extensions))[0].value
|
||||
|
||||
def get_subject_alt_name(self) -> x509.SubjectAlternativeName:
|
||||
"""Return the SubjectAlternativeName X.509 extension of the certificate."""
|
||||
return list(filter(lambda x: isinstance(x.value, x509.SubjectAlternativeName), self.cert.extensions))[0].value
|
||||
|
||||
def get_cert_as_der(self) -> bytes:
|
||||
"""Return certificate encoded as DER."""
|
||||
return self.cert.public_bytes(Encoding.DER)
|
||||
|
||||
def get_curve(self) -> ec.EllipticCurve:
|
||||
return self.cert.public_key().public_numbers().curve
|
||||
|
||||
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
def __init__(self, subject_code: str, reason_code: str, message: Optional[str] = None,
|
||||
subject_id: Optional[str] = None):
|
||||
@@ -98,6 +147,27 @@ class ApiError(Exception):
|
||||
build_resp_header(js, 'Failed', self.status_code)
|
||||
return json.dumps(js)
|
||||
|
||||
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 SmDppHttpServer:
|
||||
app = Klein()
|
||||
|
||||
@@ -136,7 +206,6 @@ class SmDppHttpServer:
|
||||
|
||||
def __init__(self, server_hostname: str, ci_certs_path: str, use_brainpool: bool = False):
|
||||
self.server_hostname = server_hostname
|
||||
self.upp_dir = os.path.realpath(os.path.join(DATA_DIR, 'upp'))
|
||||
self.ci_certs = self.load_certs_from_path(ci_certs_path)
|
||||
# load DPauth cert + key
|
||||
self.dp_auth = CertAndPrivkey(oid.id_rspRole_dp_auth_v2)
|
||||
@@ -173,6 +242,36 @@ class SmDppHttpServer:
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def b64decode_members(indict: Dict, keys: List[str]):
|
||||
"""base64-decoder all members of 'indict' whose key is in 'keys'."""
|
||||
for key in keys:
|
||||
if key in indict:
|
||||
indict[key] = b64decode(indict[key])
|
||||
|
||||
@staticmethod
|
||||
def b64encode_members(indict: Dict, keys: List[str]):
|
||||
"""base64-encoder (to string!) all members of 'indict' whose key is in 'keys'."""
|
||||
for key in keys:
|
||||
if key in indict:
|
||||
indict[key] = b64encode2str(indict[key])
|
||||
|
||||
@staticmethod
|
||||
def asn1decode_member(indict: Dict, key: str, typename: Optional[str]):
|
||||
"""decode indict[key] using RSP ASN.1 decoder for type 'typename'."""
|
||||
if not typename:
|
||||
typename = capitalize_first_char(key)
|
||||
if key in indict:
|
||||
indict[key] = rsp.asn1.decode(typename, indict[key])
|
||||
|
||||
@staticmethod
|
||||
def asn1encode_member(indict: Dict, key: str, typename: Optional[str]):
|
||||
"""encode indict[key] using RSP ASN.1 decoder for type 'typename'."""
|
||||
if not typename:
|
||||
typename = capitalize_first_char(key)
|
||||
if key in indict:
|
||||
indict[key] = rsp.asn1.encode(typename, indict[key])
|
||||
|
||||
@staticmethod
|
||||
def rsp_api_wrapper(func):
|
||||
"""Wrapper that can be used as decorator in order to perform common REST API endpoint entry/exit
|
||||
@@ -183,20 +282,16 @@ class SmDppHttpServer:
|
||||
# TODO: reject any non-JSON Content-type
|
||||
|
||||
content = json.loads(request.content.read())
|
||||
print("Rx JSON: %s" % json.dumps(content))
|
||||
print("Rx JSON: %s" % content)
|
||||
set_headers(request)
|
||||
|
||||
output = func(self, request, content)
|
||||
if output == None:
|
||||
return ''
|
||||
output = func(self, request, content) or {}
|
||||
|
||||
build_resp_header(output)
|
||||
print("Tx JSON: %s" % json.dumps(output))
|
||||
print("Tx JSON: %s" % output)
|
||||
return json.dumps(output)
|
||||
return _api_wrapper
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/initiateAuthentication', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def initiateAutentication(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ InitiateAuthentication SGP.22 Section 5.6.1"""
|
||||
# Verify that the received address matches its own SM-DP+ address, where the comparison SHALL be
|
||||
@@ -204,12 +299,11 @@ class SmDppHttpServer:
|
||||
if content['smdpAddress'] != self.server_hostname:
|
||||
raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address')
|
||||
|
||||
euiccChallenge = b64decode(content['euiccChallenge'])
|
||||
euiccChallenge = content['euiccChallenge']
|
||||
if len(euiccChallenge) != 16:
|
||||
raise ValueError
|
||||
|
||||
euiccInfo1_bin = b64decode(content['euiccInfo1'])
|
||||
euiccInfo1 = rsp.asn1.decode('EUICCInfo1', euiccInfo1_bin)
|
||||
euiccInfo1 = content['euiccInfo1']
|
||||
print("Rx euiccInfo1: %s" % euiccInfo1)
|
||||
#euiccInfo1['svn']
|
||||
|
||||
@@ -219,17 +313,7 @@ class SmDppHttpServer:
|
||||
if 'euiccCiPKIdListForSigningV3' in euiccInfo1:
|
||||
pkid_list = pkid_list + euiccInfo1['euiccCiPKIdListForSigningV3']
|
||||
# verify it supports one of the keys indicated by euiccCiPKIdListForSigning
|
||||
ci_cert = None
|
||||
for x in pkid_list:
|
||||
ci_cert = self.ci_get_cert_for_pkid(x)
|
||||
# we already support multiple CI certificates but only one set of DPauth + DPpb keys. So we must
|
||||
# make sure we choose a CI key-id which has issued both the eUICC as well as our own SM-DP side
|
||||
# certs.
|
||||
if ci_cert and cert_get_subject_key_id(ci_cert) == self.dp_auth.get_authority_key_identifier().key_identifier:
|
||||
break
|
||||
else:
|
||||
ci_cert = None
|
||||
if not ci_cert:
|
||||
if not any(self.ci_get_cert_for_pkid(x) for x in pkid_list):
|
||||
raise ApiError('8.8.2', '3.1', 'None of the proposed Public Key Identifiers is supported by the SM-DP+')
|
||||
|
||||
# TODO: Determine the set of CERT.DPauth.SIG that satisfy the following criteria:
|
||||
@@ -241,7 +325,7 @@ class SmDppHttpServer:
|
||||
|
||||
# Generate a TransactionID which is used to identify the ongoing RSP session. The TransactionID
|
||||
# SHALL be unique within the scope and lifetime of each SM-DP+.
|
||||
transactionId = uuid.uuid4().hex.upper()
|
||||
transactionId = uuid.uuid4().hex
|
||||
assert not transactionId in self.rss
|
||||
|
||||
# Generate a serverChallenge for eUICC authentication attached to the ongoing RSP session.
|
||||
@@ -258,33 +342,46 @@ class SmDppHttpServer:
|
||||
serverSigned1_bin = rsp.asn1.encode('ServerSigned1', serverSigned1)
|
||||
print("Tx serverSigned1: %s" % rsp.asn1.decode('ServerSigned1', serverSigned1_bin))
|
||||
output = {}
|
||||
output['serverSigned1'] = b64encode2str(serverSigned1_bin)
|
||||
output['serverSigned1'] = serverSigned1
|
||||
|
||||
# Generate a signature (serverSignature1) as described in section 5.7.13 "ES10b.AuthenticateServer" using the SK related to the selected CERT.DPauth.SIG.
|
||||
# serverSignature1 SHALL be created using the private key associated to the RSP Server Certificate for authentication, and verified by the eUICC using the contained public key as described in section 2.6.9. serverSignature1 SHALL apply on serverSigned1 data object.
|
||||
output['serverSignature1'] = b64encode2str(b'\x5f\x37\x40' + self.dp_auth.ecdsa_sign(serverSigned1_bin))
|
||||
output['serverSignature1'] = b'\x5f\x37\x40' + self.dp_auth.ecdsa_sign(serverSigned1_bin)
|
||||
|
||||
output['transactionId'] = transactionId
|
||||
server_cert_aki = self.dp_auth.get_authority_key_identifier()
|
||||
output['euiccCiPKIdToBeUsed'] = b64encode2str(b'\x04\x14' + server_cert_aki.key_identifier)
|
||||
output['serverCertificate'] = b64encode2str(self.dp_auth.get_cert_as_der()) # CERT.DPauth.SIG
|
||||
output['euiccCiPKIdToBeUsed'] = b'\x04\x14' + server_cert_aki.key_identifier
|
||||
output['serverCertificate'] = self.dp_auth.get_cert_as_der() # CERT.DPauth.SIG
|
||||
# FIXME: add those certificate
|
||||
#output['otherCertsInChain'] = b64encode2str()
|
||||
#output['otherCertsInChain'] = ...
|
||||
|
||||
# create SessionState and store it in rss
|
||||
self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge,
|
||||
cert_get_subject_key_id(ci_cert))
|
||||
self.rss[transactionId] = rsp.RspSessionState(transactionId, serverChallenge)
|
||||
|
||||
return output
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/authenticateClient', methods=['POST'])
|
||||
@app.route('/gsma/rsp2/es9plus/initiateAuthentication', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_initiateAuthentication(self, request: IRequest, content: dict):
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
# convert from JSON/BASE64 to decoded ASN.1
|
||||
b64decode_members(content, ['euiccChallenge', 'euiccInfo1', 'lpaRspCapability'])
|
||||
asn1decode_member(content, 'eUICCInfo1')
|
||||
|
||||
# do the actual processing
|
||||
output = self.initiateAuthentication(request, content)
|
||||
|
||||
# convert from decoded ASN.1 to base64 to JSON
|
||||
asn1encode_member(output, 'serverSigned1')
|
||||
b64encode_members(output, ['serverSigned1', 'serverSignature1', 'euiccCiPKIdToBeUsed',
|
||||
'serverCertificate', 'otherCertsInChain', 'crlList'])
|
||||
return output
|
||||
|
||||
def authenticateClient(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ AuthenticateClient in SGP.22 Section 5.6.3"""
|
||||
transactionId = content['transactionId']
|
||||
|
||||
authenticateServerResp_bin = b64decode(content['authenticateServerResponse'])
|
||||
authenticateServerResp = rsp.asn1.decode('AuthenticateServerResponse', authenticateServerResp_bin)
|
||||
authenticateServerResp = content['authenticateServerResponse']
|
||||
print("Rx %s: %s" % authenticateServerResp)
|
||||
if authenticateServerResp[0] == 'authenticateResponseError':
|
||||
r_err = authenticateServerResp[1]
|
||||
@@ -294,7 +391,8 @@ class SmDppHttpServer:
|
||||
|
||||
r_ok = authenticateServerResp[1]
|
||||
euiccSigned1 = r_ok['euiccSigned1']
|
||||
euiccSigned1_bin = rsp.extract_euiccSigned1(authenticateServerResp_bin)
|
||||
# TODO: use original data, don't re-encode?
|
||||
euiccSigned1_bin = rsp.asn1.encode('EuiccSigned1', euiccSigned1)
|
||||
euiccSignature1_bin = r_ok['euiccSignature1']
|
||||
euiccCertificate_dec = r_ok['euiccCertificate']
|
||||
# TODO: use original data, don't re-encode?
|
||||
@@ -307,35 +405,29 @@ class SmDppHttpServer:
|
||||
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
|
||||
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
|
||||
|
||||
# TODO: Verify the validity of the eUICC certificate chain
|
||||
# raise ApiError('8.1.3', '6.1', 'Verification failed')
|
||||
# raise ApiError('8.1.3', '6.3', 'Expired')
|
||||
|
||||
# TODO: Verify that the Root Certificate of the eUICC certificate chain corresponds to the
|
||||
# euiccCiPKIdToBeUsed or euiccCiPKIdToBeUsedV3
|
||||
# raise ApiError('8.11.1', '3.9', 'Unknown')
|
||||
|
||||
# Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate.
|
||||
# Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed"
|
||||
if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin):
|
||||
raise ApiError('8.1', '6.1', 'Verification failed')
|
||||
|
||||
# Verify that the transactionId is known and relates to an ongoing RSP session. Otherwise, the SM-DP+
|
||||
# SHALL return a status code "TransactionId - Unknown"
|
||||
ss = self.rss.get(transactionId, None)
|
||||
if ss is None:
|
||||
raise ApiError('8.10.1', '3.9', 'Unknown')
|
||||
ss.euicc_cert = euicc_cert
|
||||
ss.eum_cert = eum_cert # TODO: do we need this in the state?
|
||||
|
||||
# Verify that the Root Certificate of the eUICC certificate chain corresponds to the
|
||||
# euiccCiPKIdToBeUsed or TODO: euiccCiPKIdToBeUsedV3
|
||||
if cert_get_auth_key_id(eum_cert) != ss.ci_cert_id:
|
||||
raise ApiError('8.11.1', '3.9', 'Unknown')
|
||||
|
||||
# Verify the validity of the eUICC certificate chain
|
||||
cs = CertificateSet(self.ci_get_cert_for_pkid(ss.ci_cert_id))
|
||||
cs.add_intermediate_cert(eum_cert)
|
||||
# TODO v3: otherCertsInChain
|
||||
try:
|
||||
cs.verify_cert_chain(euicc_cert)
|
||||
except VerifyError:
|
||||
raise ApiError('8.1.3', '6.1', 'Verification failed (certificate chain)')
|
||||
# raise ApiError('8.1.3', '6.3', 'Expired')
|
||||
|
||||
|
||||
# Verify euiccSignature1 over euiccSigned1 using pubkey from euiccCertificate.
|
||||
# Otherwise, the SM-DP+ SHALL return a status code "eUICC - Verification failed"
|
||||
if not self._ecdsa_verify(euicc_cert, euiccSignature1_bin, euiccSigned1_bin):
|
||||
raise ApiError('8.1', '6.1', 'Verification failed (euiccSignature1 over euiccSigned1)')
|
||||
ss.eum_cert = eum_cert # do we need this in the state?
|
||||
|
||||
# TODO: verify eUICC cert is signed by EUM cert
|
||||
# TODO: verify EUM cert is signed by CI cert
|
||||
# TODO: verify EID of eUICC cert is within permitted range of EUM cert
|
||||
|
||||
ss.eid = ss.euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
|
||||
@@ -345,40 +437,10 @@ class SmDppHttpServer:
|
||||
# serverChallenge returned by the eUICC. Otherwise, the SM-DP+ SHALL return a status code "eUICC -
|
||||
# Verification failed".
|
||||
if euiccSigned1['serverChallenge'] != ss.serverChallenge:
|
||||
raise ApiError('8.1', '6.1', 'Verification failed (serverChallenge)')
|
||||
|
||||
# If ctxParams1 contains a ctxParamsForCommonAuthentication data object, the SM-DP+ Shall [...]
|
||||
# TODO: We really do a very simplistic job here, this needs to be properly implemented later,
|
||||
# considering all the various cases, profile state, etc.
|
||||
if euiccSigned1['ctxParams1'][0] == 'ctxParamsForCommonAuthentication':
|
||||
cpca = euiccSigned1['ctxParams1'][1]
|
||||
matchingId = cpca.get('matchingId', None)
|
||||
if not matchingId:
|
||||
# TODO: check if any pending profile downloads for the EID
|
||||
raise ApiError('8.2.6', '3.8', 'Refused')
|
||||
if matchingId:
|
||||
# look up profile based on matchingID. We simply check if a given file exists for now..
|
||||
path = os.path.join(self.upp_dir, matchingId) + '.der'
|
||||
# prevent directory traversal attack
|
||||
if os.path.commonprefix((os.path.realpath(path),self.upp_dir)) != self.upp_dir:
|
||||
raise ApiError('8.2.6', '3.8', 'Refused')
|
||||
if not os.path.isfile(path) or not os.access(path, os.R_OK):
|
||||
raise ApiError('8.2.6', '3.8', 'Refused')
|
||||
ss.matchingId = matchingId
|
||||
with open(path, 'rb') as f:
|
||||
pes = saip.ProfileElementSequence.from_der(f.read())
|
||||
iccid_str = b2h(pes.get_pe_for_type('header').decoded['iccid'])
|
||||
else:
|
||||
# there's currently no other option in the ctxParams1 choice, so this cannot happen
|
||||
raise ApiError('1.3.1', '2.2', 'ctxParams1 missing mandatory ctxParamsForCommonAuthentication')
|
||||
|
||||
# FIXME: we actually want to perform the profile binding herr, and read the profile metadat from the profile
|
||||
raise ApiError('8.1', '6.1', 'Verification failed')
|
||||
|
||||
# Put together profileMetadata + _bin
|
||||
ss.profileMetadata = ProfileMetadata(iccid_bin=h2b(swap_nibbles(iccid_str)), spn="OsmocomSPN", profile_name=matchingId)
|
||||
# enable notifications for all operations
|
||||
for event in ['enable', 'disable', 'delete']:
|
||||
ss.profileMetadata.add_notification(event, self.server_hostname)
|
||||
ss.profileMetadata = ProfileMetadata(iccid_bin= h2b(swap_nibbles('89000123456789012358')), spn="OsmocomSPN", profile_name="OsmocomProfile")
|
||||
profileMetadata_bin = ss.profileMetadata.gen_store_metadata_request()
|
||||
|
||||
# Put together smdpSigned2 + _bin
|
||||
@@ -395,14 +457,26 @@ class SmDppHttpServer:
|
||||
self.rss[transactionId] = ss
|
||||
return {
|
||||
'transactionId': transactionId,
|
||||
'profileMetadata': b64encode2str(profileMetadata_bin),
|
||||
'smdpSigned2': b64encode2str(smdpSigned2_bin),
|
||||
'smdpSignature2': b64encode2str(ss.smdpSignature2_do),
|
||||
'smdpCertificate': b64encode2str(self.dp_pb.get_cert_as_der()), # CERT.DPpb.SIG
|
||||
'profileMetadata': profileMetadata,
|
||||
'smdpSigned2': smdpSigned2,
|
||||
'smdpSignature2': ss.smdpSignature2_do,
|
||||
'smdpCertificate': self.dp_pb.get_cert_as_der(), # CERT.DPpb.SIG
|
||||
}
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/getBoundProfilePackage', methods=['POST'])
|
||||
@app.route('/gsma/rsp2/es9plus/authenticateClient', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_authenticateClient(self, request: IRequest, content: dict):
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
b64decode_members(content, ['authenticateServerResponse', 'deleteNotifciationForDc'])
|
||||
asn1decode_member(content, 'authenticateServerResponse')
|
||||
|
||||
output = self.authenticateClient(requeset, content)
|
||||
|
||||
asn1encode_member(output, 'smdpSigned2')
|
||||
b64encode_members(output, ['profileMetadata', 'smdpSigned2', 'smdpSignature2', 'smdpCertificate'])
|
||||
return output
|
||||
|
||||
|
||||
def getBoundProfilePackage(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ GetBoundProfilePackage SGP.22 Section 5.6.2"""
|
||||
transactionId = content['transactionId']
|
||||
@@ -412,8 +486,7 @@ class SmDppHttpServer:
|
||||
if not ss:
|
||||
raise ApiError('8.10.1', '3.9', 'The RSP session identified by the TransactionID is unknown')
|
||||
|
||||
prepDownloadResp_bin = b64decode(content['prepareDownloadResponse'])
|
||||
prepDownloadResp = rsp.asn1.decode('PrepareDownloadResponse', prepDownloadResp_bin)
|
||||
prepDownloadResp = content['prepareDownloadResponse']
|
||||
print("Rx %s: %s" % prepDownloadResp)
|
||||
|
||||
if prepDownloadResp[0] == 'downloadResponseError':
|
||||
@@ -426,7 +499,8 @@ class SmDppHttpServer:
|
||||
|
||||
# Verify the euiccSignature2 computed over euiccSigned2 and smdpSignature2 using the PK.EUICC.SIG attached to the ongoing RSP session
|
||||
euiccSigned2 = r_ok['euiccSigned2']
|
||||
euiccSigned2_bin = rsp.extract_euiccSigned2(prepDownloadResp_bin)
|
||||
# TODO: use original data, don't re-encode?
|
||||
euiccSigned2_bin = rsp.asn1.encode('EUICCSigned2', euiccSigned2)
|
||||
if not self._ecdsa_verify(ss.euicc_cert, r_ok['euiccSignature2'], euiccSigned2_bin + ss.smdpSignature2_do):
|
||||
raise ApiError('8.1', '6.1', 'eUICC signature is invalid')
|
||||
|
||||
@@ -449,7 +523,7 @@ class SmDppHttpServer:
|
||||
|
||||
ss.host_id = b'mahlzeit'
|
||||
|
||||
# Generate Session Keys using the CRT, otPK.eUICC.ECKA and otSK.DP.ECKA according to annex G
|
||||
# Generate Session Keys using the CRT, opPK.eUICC.ECKA and otSK.DP.ECKA according to annex G
|
||||
euicc_public_key = ec.EllipticCurvePublicKey.from_encoded_point(ss.smdp_ot.curve, ss.euicc_otpk)
|
||||
ss.shared_secret = ss.smdp_ot.exchange(ec.ECDH(), euicc_public_key)
|
||||
print("shared_secret: %s" % b2h(ss.shared_secret))
|
||||
@@ -457,7 +531,7 @@ class SmDppHttpServer:
|
||||
# TODO: Check if this order requires a Confirmation Code verification
|
||||
|
||||
# Perform actual protection + binding of profile package (or return pre-bound one)
|
||||
with open(os.path.join(self.upp_dir, ss.matchingId)+'.der', 'rb') as f:
|
||||
with open(os.path.join(DATA_DIR, 'upp', 'TS48 V2 eSIM_GTP_SAIP2.1_NoBERTLV.rename2der'), 'rb') as f:
|
||||
upp = UnprotectedProfilePackage.from_der(f.read(), metadata=ss.profileMetadata)
|
||||
# HACK: Use empty PPP as we're still debuggin the configureISDP step, and we want to avoid
|
||||
# cluttering the log with stuff happening after the failure
|
||||
@@ -474,18 +548,26 @@ class SmDppHttpServer:
|
||||
self.rss[transactionId] = ss
|
||||
return {
|
||||
'transactionId': transactionId,
|
||||
'boundProfilePackage': b64encode2str(bpp.encode(ss, self.dp_pb)),
|
||||
'boundProfilePackage': bpp.encode(ss, self.dp_pb),
|
||||
}
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/handleNotification', methods=['POST'])
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/getBoundProfilePackage', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_getBoundProfilePackage(self, request: IRequest, content: dict):
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
b64decode_members(content, ['prepareDownloadResponse'])
|
||||
asn1decode_member(content, 'prepareDownlaodResponse')
|
||||
|
||||
output = self.getBoundProfilePackage(request, content)
|
||||
|
||||
asn1encode_member(output, 'boundProfilePackage')
|
||||
b64encode_members(output, ['boundProfilePackage'])
|
||||
return output
|
||||
|
||||
def handleNotification(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ HandleNotification in SGP.22 Section 5.6.4"""
|
||||
# SGP.22 Section 6.3: "A normal notification function execution status (MEP Notification)
|
||||
# SHALL be indicated by the HTTP status code '204' (No Content) with an empty HTTP response body"
|
||||
request.setResponseCode(204)
|
||||
pendingNotification_bin = b64decode(content['pendingNotification'])
|
||||
pendingNotification = rsp.asn1.decode('PendingNotification', pendingNotification_bin)
|
||||
pendingNotification = content['pendingNotification']
|
||||
print("Rx %s: %s" % pendingNotification)
|
||||
if pendingNotification[0] == 'profileInstallationResult':
|
||||
profileInstallRes = pendingNotification[1]
|
||||
@@ -500,46 +582,36 @@ class SmDppHttpServer:
|
||||
pird_bin = rsp.asn1.encode('ProfileInstallationResultData', pird)
|
||||
# verify eUICC signature
|
||||
if not self._ecdsa_verify(ss.euicc_cert, profileInstallRes['euiccSignPIR'], pird_bin):
|
||||
raise Exception('ECDSA signature verification failed on notification')
|
||||
print("Unable to verify eUICC signature")
|
||||
print("Profile Installation Final Result: ", pird['finalResult'])
|
||||
# remove session state
|
||||
del self.rss[transactionId]
|
||||
elif pendingNotification[0] == 'otherSignedNotification':
|
||||
otherSignedNotif = pendingNotification[1]
|
||||
# TODO: use some kind of partially-parsed original data, don't re-encode?
|
||||
euiccCertificate_bin = rsp.asn1.encode('Certificate', otherSignedNotif['euiccCertificate'])
|
||||
eumCertificate_bin = rsp.asn1.encode('Certificate', otherSignedNotif['eumCertificate'])
|
||||
euicc_cert = x509.load_der_x509_certificate(euiccCertificate_bin)
|
||||
eum_cert = x509.load_der_x509_certificate(eumCertificate_bin)
|
||||
ci_cert_id = cert_get_auth_key_id(eum_cert)
|
||||
# Verify the validity of the eUICC certificate chain
|
||||
cs = CertificateSet(self.ci_get_cert_for_pkid(ci_cert_id))
|
||||
cs.add_intermediate_cert(eum_cert)
|
||||
# TODO v3: otherCertsInChain
|
||||
cs.verify_cert_chain(euicc_cert)
|
||||
tbs_bin = rsp.asn1.encode('NotificationMetadata', otherSignedNotif['tbsOtherNotification'])
|
||||
if not self._ecdsa_verify(euicc_cert, otherSignedNotif['euiccNotificationSignature'], tbs_bin):
|
||||
raise Exception('ECDSA signature verification failed on notification')
|
||||
other_notif = otherSignedNotif['tbsOtherNotification']
|
||||
pmo = PMO.from_bitstring(other_notif['profileManagementOperation'])
|
||||
eid = euicc_cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
|
||||
iccid = other_notif.get('iccid', None)
|
||||
if iccid:
|
||||
iccid = swap_nibbles(b2h(iccid))
|
||||
print("handleNotification: EID %s: %s of %s" % (eid, pmo, iccid))
|
||||
# TODO
|
||||
pass
|
||||
else:
|
||||
raise ValueError(pendingNotification)
|
||||
return None
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/handleNotification', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_handleNotification(self, request: IRequest, content: dict):
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
b64decode_members(content, ['pendingNotification'])
|
||||
asn1decode_member(content, 'pendingNotification')
|
||||
|
||||
output = self.handleNotification(request, content)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
#@app.route('/gsma/rsp3/es9plus/handleDeviceChangeRequest, methods=['POST']')
|
||||
#@rsp_api_wrapper
|
||||
#"""See ES9+ ConfirmDeviceChange in SGP.22 Section 5.6.6"""
|
||||
# TODO: implement this
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/cancelSession', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def cancelSession(self, request: IRequest, content: dict) -> dict:
|
||||
"""See ES9+ CancelSession in SGP.22 Section 5.6.5"""
|
||||
print("Rx JSON: %s" % content)
|
||||
transactionId = content['transactionId']
|
||||
|
||||
# Verify that the received transactionId is known and relates to an ongoing RSP session
|
||||
@@ -547,8 +619,7 @@ class SmDppHttpServer:
|
||||
if ss is None:
|
||||
raise ApiError('8.10.1', '3.9', 'The RSP session identified by the transactionId is unknown')
|
||||
|
||||
cancelSessionResponse_bin = b64decode(content['cancelSessionResponse'])
|
||||
cancelSessionResponse = rsp.asn1.decode('CancelSessionResponse', cancelSessionResponse_bin)
|
||||
cancelSessionResponse = content['cancelSessionResponse']
|
||||
print("Rx %s: %s" % cancelSessionResponse)
|
||||
|
||||
if cancelSessionResponse[0] == 'cancelSessionResponseError':
|
||||
@@ -578,18 +649,66 @@ class SmDppHttpServer:
|
||||
del self.rss[transactionId]
|
||||
return { 'transactionId': transactionId }
|
||||
|
||||
@app.route('/gsma/rsp2/es9plus/cancelSession', methods=['POST'])
|
||||
@rsp_api_wrapper
|
||||
def json_cancelSessionn(self, request: IRequest, content: dict) -> dict:
|
||||
"""Transform from JSON binding to generic function and back."""
|
||||
b64decode_members(content, ['cancelSessionResponse'])
|
||||
asn1decode_member(content, 'cancelSessionResponse')
|
||||
|
||||
output = self.cancelSession(request, content)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
@app.route('/gsma/rsp2/asn1', methods=['POST'])
|
||||
def asn1(self, request: IRequest) -> dict:
|
||||
# TODO: evaluate User-Agent + X-Admin-Protocol header
|
||||
# TODO: reject any non-ASN.1 Content-type
|
||||
|
||||
content = rsp.asn1.decode('RemoteProfileProvisioningRequest', request.content.read())
|
||||
print("Rx ASN.1: %s" % content)
|
||||
request.setHeader('Content-Type', 'application/x-gsma-rsp-asn1')
|
||||
request.setHeader('X-Admin-Protocol', 'gsma/rsp/v2.1.0')
|
||||
|
||||
operation = content[0]
|
||||
if operation == 'initiateAuthenticationRequest':
|
||||
method = self.initiateAuthentication
|
||||
ochoice = 'initiateAuthenticationResponse'
|
||||
elif operation == 'authenticateClientRequest':
|
||||
method = self.authenticateClient
|
||||
ochoice = 'authenticateClientResponseEs9'
|
||||
elif operation == 'getBoundProfilePackageRequest':
|
||||
method = self.getBoundProfilePackage
|
||||
ochoice = 'getBoundProfilePackageResponse'
|
||||
elif operation == 'cancelSessionRequestEs9':
|
||||
method = self.cancelSession
|
||||
ochoice = 'cancelSessionResponseEs9'
|
||||
elif operation == 'handleNotification':
|
||||
method = self.handleNotification
|
||||
ochoice = None
|
||||
|
||||
output = method(self, request, content[1])
|
||||
|
||||
if ochoice:
|
||||
output = (ochoice, output)
|
||||
print("Tx ASN.1: %s" % output)
|
||||
return rsp.asn1.encode('RemoteProfileProvisioningResponse', output)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def main(argv):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-H", "--host", help="Host/IP to bind HTTP to", default="localhost")
|
||||
parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000)
|
||||
#parser.add_argument("-H", "--host", help="Host/IP to bind HTTP to", default="localhost")
|
||||
#parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000)
|
||||
#parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=False)
|
||||
hs = SmDppHttpServer(HOSTNAME, os.path.join(DATA_DIR, 'certs', 'CertificateIssuer'), use_brainpool=True)
|
||||
#hs.app.run(endpoint_description="ssl:port=8000:dhParameters=dh_param_2048.pem")
|
||||
hs.app.run(args.host, args.port)
|
||||
hs.app.run("localhost", 8000)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv)
|
||||
|
||||
154
ota_test.py
154
ota_test.py
@@ -1,154 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from pySim.ota import *
|
||||
from pySim.sms import SMS_SUBMIT, SMS_DELIVER, AddressField
|
||||
from pySim.utils import h2b, h2b
|
||||
|
||||
# pre-defined SPI values for use in test cases below
|
||||
SPI_CC_POR_CIPHERED_CC = {
|
||||
'counter':'no_counter',
|
||||
'ciphering':True,
|
||||
'rc_cc_ds': 'cc',
|
||||
'por_in_submit':False,
|
||||
'por_shall_be_ciphered':True,
|
||||
'por_rc_cc_ds': 'cc',
|
||||
'por': 'por_required'
|
||||
}
|
||||
|
||||
SPI_CC_POR_UNCIPHERED_CC = {
|
||||
'counter':'no_counter',
|
||||
'ciphering':True,
|
||||
'rc_cc_ds': 'cc',
|
||||
'por_in_submit':False,
|
||||
'por_shall_be_ciphered':False,
|
||||
'por_rc_cc_ds': 'cc',
|
||||
'por': 'por_required'
|
||||
}
|
||||
|
||||
SPI_CC_POR_UNCIPHERED_NOCC = {
|
||||
'counter':'no_counter',
|
||||
'ciphering':True,
|
||||
'rc_cc_ds': 'cc',
|
||||
'por_in_submit':False,
|
||||
'por_shall_be_ciphered':False,
|
||||
'por_rc_cc_ds': 'no_rc_cc_ds',
|
||||
'por': 'por_required'
|
||||
}
|
||||
|
||||
# SJA5 SAMPLE cards provisioned by execute_ipr.py
|
||||
OTA_KEYSET_SJA5_SAMPLES = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=3,
|
||||
algo_auth='triple_des_cbc2', kid_idx=3,
|
||||
kic=h2b('300102030405060708090a0b0c0d0e0f'),
|
||||
kid=h2b('301102030405060708090a0b0c0d0e0f'))
|
||||
|
||||
OTA_KEYSET_SJA5_AES128 = OtaKeyset(algo_crypt='aes_cbc', kic_idx=2,
|
||||
algo_auth='aes_cmac', kid_idx=2,
|
||||
kic=h2b('200102030405060708090a0b0c0d0e0f'),
|
||||
kid=h2b('201102030405060708090a0b0c0d0e0f'))
|
||||
|
||||
# TS.48 profile on sysmoEUICC1-C2G
|
||||
OTA_KEYSET_C2G_AES128 = OtaKeyset(algo_crypt='aes_cbc', kic_idx=2,
|
||||
algo_auth='aes_cmac', kid_idx=2,
|
||||
kic=h2b('66778899AABBCCDD1122334455EEFF10'),
|
||||
kid=h2b('112233445566778899AABBCCDDEEFF10'))
|
||||
|
||||
|
||||
# ISD-R on sysmoEUICC1-C2G
|
||||
OTA_KEYSET_C2G_AES128_ISDR = OtaKeyset(algo_crypt='aes_cbc', kic_idx=1,
|
||||
algo_auth='aes_cmac', kid_idx=1,
|
||||
kic=h2b('B52F9C5938D1C19ED73E1AE772937FD7'),
|
||||
kid=h2b('3BC696ACD1EEC95A6624F7330D22FC81'))
|
||||
|
||||
# ISD-A on sysmoEUICC1-C2G
|
||||
OTA_KEYSET_C2G_AES128_ISDA = OtaKeyset(algo_crypt='aes_cbc', kic_idx=1,
|
||||
algo_auth='aes_cmac', kid_idx=1,
|
||||
kic=h2b('8DAAD1DAAA8D7C9000E3BBED8B7556E7'),
|
||||
kid=h2b('5392D503AE050DDEAF81AFAEFF275A2B'))
|
||||
|
||||
# TODO: AES192
|
||||
# TODO: AES256
|
||||
|
||||
testcases = [
|
||||
{
|
||||
'name': '3DES-SJA5-CIPHERED-CC',
|
||||
'ota_keyset': OTA_KEYSET_SJA5_SAMPLES,
|
||||
'spi': SPI_CC_POR_CIPHERED_CC,
|
||||
'request': {
|
||||
'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
|
||||
'encoded_cmd': '00201506193535b00011ae733256918d050b87c94fbfe12e4dc402f262c41cf67f2f',
|
||||
'encoded_tpdu': '400881214365877ff6227052000000000302700000201506193535b00011ae733256918d050b87c94fbfe12e4dc402f262c41cf67f2f',
|
||||
},
|
||||
'response': {
|
||||
'encoded_resp': '027100001c12b000118bb989492c632529326a2f4681feb37c825bc9021c9f6d0b',
|
||||
}
|
||||
}, {
|
||||
'name': '3DES-SJA5-UNCIPHERED-CC',
|
||||
'ota_keyset': OTA_KEYSET_SJA5_SAMPLES,
|
||||
'spi': SPI_CC_POR_UNCIPHERED_CC,
|
||||
'request': {
|
||||
'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
|
||||
'encoded_cmd': '00201506093535b00011c49ac91ab8159ba5b83a54fb6385e0a5e31694f8b215fafc',
|
||||
'encoded_tpdu': '400881214365877ff6227052000000000302700000201506093535b00011c49ac91ab8159ba5b83a54fb6385e0a5e31694f8b215fafc',
|
||||
},
|
||||
'response': {
|
||||
'encoded_resp': '027100001612b0001100000000000000b5bcd6353a421fae016132',
|
||||
}
|
||||
}, {
|
||||
'name': '3DES-SJA5-UNCIPHERED-NOCC',
|
||||
'ota_keyset': OTA_KEYSET_SJA5_SAMPLES,
|
||||
'spi': SPI_CC_POR_UNCIPHERED_NOCC,
|
||||
'request': {
|
||||
'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
|
||||
'encoded_cmd': '00201506013535b000113190be334900f52b025f3f7eddfe868e96ebf310023b7769',
|
||||
'encoded_tpdu': '400881214365877ff6227052000000000302700000201506013535b000113190be334900f52b025f3f7eddfe868e96ebf310023b7769',
|
||||
},
|
||||
'response': {
|
||||
'encoded_resp': '027100000e0ab0001100000000000000016132',
|
||||
}
|
||||
}, {
|
||||
'name': 'AES128-C2G-CIPHERED-CC',
|
||||
'ota_keyset': OTA_KEYSET_C2G_AES128_ISDR,
|
||||
'spi': SPI_CC_POR_CIPHERED_CC,
|
||||
'request': {
|
||||
#'apdu': b'\x00\xa4\x00\x04\x02\x3f\x00',
|
||||
'apdu': h2b('80ec800300'),
|
||||
'encoded_cmd': '00281506192222b00011e87cceebb2d93083011ce294f93fc4d8de80da1abae8c37ca3e72ec4432e5058',
|
||||
'encoded_tpdu': '400881214365877ff6227052000000000302700000281506192222b00011e87cceebb2d93083011ce294f93fc4d8de80da1abae8c37ca3e72ec4432e5058',
|
||||
},
|
||||
'response': {
|
||||
'encoded_resp': '027100002412b00011ebc6b497e2cad7aedf36ace0e3a29b38853f0fe9ccde81913be5702b73abce1f',
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
for t in testcases:
|
||||
print()
|
||||
print("==== TESTCASE: %s" % t['name'])
|
||||
od = t['ota_keyset']
|
||||
|
||||
# RAM: B00000
|
||||
# SIM RFM: B00010
|
||||
# USIM RFM: B00011
|
||||
# ISD-R: 000001
|
||||
# ECASD: 000002
|
||||
tar = h2b('000001')
|
||||
|
||||
dialect = OtaDialectSms()
|
||||
outp = dialect.encode_cmd(od, tar, t['spi'], apdu=t['request']['apdu'])
|
||||
print("result: %s" % b2h(outp))
|
||||
#assert(b2h(outp) == t['request']['encoded_cmd'])
|
||||
|
||||
with_udh = b'\x02\x70\x00' + outp
|
||||
print("with_udh: %s" % b2h(with_udh))
|
||||
|
||||
|
||||
# processing of the response from the card
|
||||
da = AddressField('12345678', 'unknown', 'isdn_e164')
|
||||
#tpdu = SMS_SUBMIT(tp_udhi=True, tp_mr=0x23, tp_da=da, tp_pid=0x7F, tp_dcs=0xF6, tp_udl=3, tp_ud=with_udh)
|
||||
tpdu = SMS_DELIVER(tp_udhi=True, tp_oa=da, tp_pid=0x7F, tp_dcs=0xF6, tp_scts=h2b('22705200000000'), tp_udl=3, tp_ud=with_udh)
|
||||
print("TPDU: %s" % tpdu)
|
||||
print("tpdu: %s" % b2h(tpdu.to_bytes()))
|
||||
#assert(b2h(tpdu.to_bytes()) == t['request']['encoded_tpdu'])
|
||||
|
||||
r = dialect.decode_resp(od, t['spi'], t['response']['encoded_resp'])
|
||||
print("RESP: ", r)
|
||||
154
pySim-prog.py
154
pySim-prog.py
@@ -25,7 +25,7 @@
|
||||
#
|
||||
|
||||
import hashlib
|
||||
import argparse
|
||||
from optparse import OptionParser
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
@@ -33,12 +33,11 @@ import sys
|
||||
import traceback
|
||||
import json
|
||||
import csv
|
||||
from osmocom.utils import h2b, swap_nibbles, rpad
|
||||
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.transport import init_reader, argparse_add_reader_args
|
||||
from pySim.transport import init_reader
|
||||
from pySim.legacy.cards import _cards_classes, card_detect
|
||||
from pySim.utils import derive_milenage_opc, calculate_luhn, dec_iccid
|
||||
from pySim.utils import h2b, swap_nibbles, rpad, derive_milenage_opc, calculate_luhn, dec_iccid
|
||||
from pySim.ts_51_011 import EF_AD
|
||||
from pySim.legacy.ts_51_011 import EF
|
||||
from pySim.card_handler import *
|
||||
@@ -47,146 +46,169 @@ from pySim.utils import *
|
||||
|
||||
def parse_options():
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
argparse_add_reader_args(parser)
|
||||
parser = OptionParser(usage="usage: %prog [options]")
|
||||
|
||||
parser.add_argument("-t", "--type", dest="type",
|
||||
help="Card type (user -t list to view) [default: %(default)s]",
|
||||
parser.add_option("-d", "--device", dest="device", metavar="DEV",
|
||||
help="Serial Device for SIM access [default: %default]",
|
||||
default="/dev/ttyUSB0",
|
||||
)
|
||||
parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
|
||||
help="Baudrate used for SIM access [default: %default]",
|
||||
default=9600,
|
||||
)
|
||||
parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", type='int', metavar="PCSC",
|
||||
help="Which PC/SC reader number for SIM access",
|
||||
default=None,
|
||||
)
|
||||
parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",
|
||||
help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",
|
||||
default=None,
|
||||
)
|
||||
parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",
|
||||
help="Baudrate used for modem's port [default: %default]",
|
||||
default=115200,
|
||||
)
|
||||
parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",
|
||||
help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",
|
||||
default=None,
|
||||
)
|
||||
parser.add_option("-t", "--type", dest="type",
|
||||
help="Card type (user -t list to view) [default: %default]",
|
||||
default="auto",
|
||||
)
|
||||
parser.add_argument("-T", "--probe", dest="probe",
|
||||
parser.add_option("-T", "--probe", dest="probe",
|
||||
help="Determine card type",
|
||||
default=False, action="store_true"
|
||||
)
|
||||
parser.add_argument("-a", "--pin-adm", dest="pin_adm",
|
||||
parser.add_option("-a", "--pin-adm", dest="pin_adm",
|
||||
help="ADM PIN used for provisioning (overwrites default)",
|
||||
)
|
||||
parser.add_argument("-A", "--pin-adm-hex", dest="pin_adm_hex",
|
||||
parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
|
||||
help="ADM PIN used for provisioning, as hex string (16 characters long",
|
||||
)
|
||||
parser.add_argument("-e", "--erase", dest="erase", action='store_true',
|
||||
help="Erase beforehand [default: %(default)s]",
|
||||
parser.add_option("-e", "--erase", dest="erase", action='store_true',
|
||||
help="Erase beforehand [default: %default]",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument("-S", "--source", dest="source",
|
||||
help="Data Source[default: %(default)s]",
|
||||
parser.add_option("-S", "--source", dest="source",
|
||||
help="Data Source[default: %default]",
|
||||
default="cmdline",
|
||||
)
|
||||
|
||||
# if mode is "cmdline"
|
||||
parser.add_argument("-n", "--name", dest="name",
|
||||
help="Operator name [default: %(default)s]",
|
||||
parser.add_option("-n", "--name", dest="name",
|
||||
help="Operator name [default: %default]",
|
||||
default="Magic",
|
||||
)
|
||||
parser.add_argument("-c", "--country", dest="country", type=int, metavar="CC",
|
||||
help="Country code [default: %(default)s]",
|
||||
parser.add_option("-c", "--country", dest="country", type="int", metavar="CC",
|
||||
help="Country code [default: %default]",
|
||||
default=1,
|
||||
)
|
||||
parser.add_argument("-x", "--mcc", dest="mcc",
|
||||
help="Mobile Country Code [default: %(default)s]",
|
||||
parser.add_option("-x", "--mcc", dest="mcc", type="string",
|
||||
help="Mobile Country Code [default: %default]",
|
||||
default="901",
|
||||
)
|
||||
parser.add_argument("-y", "--mnc", dest="mnc",
|
||||
help="Mobile Network Code [default: %(default)s]",
|
||||
parser.add_option("-y", "--mnc", dest="mnc", type="string",
|
||||
help="Mobile Network Code [default: %default]",
|
||||
default="55",
|
||||
)
|
||||
parser.add_argument("--mnclen", dest="mnclen",
|
||||
help="Length of Mobile Network Code [default: %(default)s]",
|
||||
parser.add_option("--mnclen", dest="mnclen", type="choice",
|
||||
help="Length of Mobile Network Code [default: %default]",
|
||||
default="auto",
|
||||
choices=["2", "3", "auto"],
|
||||
)
|
||||
parser.add_argument("-m", "--smsc", dest="smsc",
|
||||
parser.add_option("-m", "--smsc", dest="smsc",
|
||||
help="SMSC number (Start with + for international no.) [default: '00 + country code + 5555']",
|
||||
)
|
||||
parser.add_argument("-M", "--smsp", dest="smsp",
|
||||
parser.add_option("-M", "--smsp", dest="smsp",
|
||||
help="Raw SMSP content in hex [default: auto from SMSC]",
|
||||
)
|
||||
|
||||
parser.add_argument("-s", "--iccid", dest="iccid", metavar="ID",
|
||||
parser.add_option("-s", "--iccid", dest="iccid", metavar="ID",
|
||||
help="Integrated Circuit Card ID",
|
||||
)
|
||||
parser.add_argument("-i", "--imsi", dest="imsi",
|
||||
parser.add_option("-i", "--imsi", dest="imsi",
|
||||
help="International Mobile Subscriber Identity",
|
||||
)
|
||||
parser.add_argument("--msisdn", dest="msisdn",
|
||||
parser.add_option("--msisdn", dest="msisdn",
|
||||
help="Mobile Subscriber Integrated Services Digital Number",
|
||||
)
|
||||
parser.add_argument("-k", "--ki", dest="ki",
|
||||
parser.add_option("-k", "--ki", dest="ki",
|
||||
help="Ki (default is to randomize)",
|
||||
)
|
||||
parser.add_argument("-o", "--opc", dest="opc",
|
||||
parser.add_option("-o", "--opc", dest="opc",
|
||||
help="OPC (default is to randomize)",
|
||||
)
|
||||
parser.add_argument("--op", dest="op",
|
||||
parser.add_option("--op", dest="op",
|
||||
help="Set OP to derive OPC from OP and KI",
|
||||
)
|
||||
parser.add_argument("--acc", dest="acc",
|
||||
parser.add_option("--acc", dest="acc",
|
||||
help="Set ACC bits (Access Control Code). not all card types are supported",
|
||||
)
|
||||
parser.add_argument("--opmode", dest="opmode",
|
||||
parser.add_option("--opmode", dest="opmode", type="choice",
|
||||
help="Set UE Operation Mode in EF.AD (Administrative Data)",
|
||||
default=None,
|
||||
choices=['{:02X}'.format(int(m)) for m in EF_AD.OP_MODE],
|
||||
)
|
||||
parser.add_argument("-f", "--fplmn", dest="fplmn", action="append",
|
||||
parser.add_option("-f", "--fplmn", dest="fplmn", action="append",
|
||||
help="Set Forbidden PLMN. Add multiple time for multiple FPLMNS",
|
||||
)
|
||||
parser.add_argument("--epdgid", dest="epdgid",
|
||||
parser.add_option("--epdgid", dest="epdgid",
|
||||
help="Set Home Evolved Packet Data Gateway (ePDG) Identifier. (Only FQDN format supported)",
|
||||
)
|
||||
parser.add_argument("--epdgSelection", dest="epdgSelection",
|
||||
parser.add_option("--epdgSelection", dest="epdgSelection",
|
||||
help="Set PLMN for ePDG Selection Information. (Only Operator Identifier FQDN format supported)",
|
||||
)
|
||||
parser.add_argument("--pcscf", dest="pcscf",
|
||||
parser.add_option("--pcscf", dest="pcscf",
|
||||
help="Set Proxy Call Session Control Function (P-CSCF) Address. (Only FQDN format supported)",
|
||||
)
|
||||
parser.add_argument("--ims-hdomain", dest="ims_hdomain",
|
||||
parser.add_option("--ims-hdomain", dest="ims_hdomain",
|
||||
help="Set IMS Home Network Domain Name in FQDN format",
|
||||
)
|
||||
parser.add_argument("--impi", dest="impi",
|
||||
parser.add_option("--impi", dest="impi",
|
||||
help="Set IMS private user identity",
|
||||
)
|
||||
parser.add_argument("--impu", dest="impu",
|
||||
parser.add_option("--impu", dest="impu",
|
||||
help="Set IMS public user identity",
|
||||
)
|
||||
parser.add_argument("--read-imsi", dest="read_imsi", action="store_true",
|
||||
parser.add_option("--read-imsi", dest="read_imsi", action="store_true",
|
||||
help="Read the IMSI from the CARD", default=False
|
||||
)
|
||||
parser.add_argument("--read-iccid", dest="read_iccid", action="store_true",
|
||||
parser.add_option("--read-iccid", dest="read_iccid", action="store_true",
|
||||
help="Read the ICCID from the CARD", default=False
|
||||
)
|
||||
parser.add_argument("-z", "--secret", dest="secret", metavar="STR",
|
||||
parser.add_option("-z", "--secret", dest="secret", metavar="STR",
|
||||
help="Secret used for ICCID/IMSI autogen",
|
||||
)
|
||||
parser.add_argument("-j", "--num", dest="num", type=int,
|
||||
parser.add_option("-j", "--num", dest="num", type=int,
|
||||
help="Card # used for ICCID/IMSI autogen",
|
||||
)
|
||||
parser.add_argument("--batch", dest="batch_mode",
|
||||
help="Enable batch mode [default: %(default)s]",
|
||||
parser.add_option("--batch", dest="batch_mode",
|
||||
help="Enable batch mode [default: %default]",
|
||||
default=False, action='store_true',
|
||||
)
|
||||
parser.add_argument("--batch-state", dest="batch_state", metavar="FILE",
|
||||
parser.add_option("--batch-state", dest="batch_state", metavar="FILE",
|
||||
help="Optional batch state file",
|
||||
)
|
||||
|
||||
# if mode is "csv"
|
||||
parser.add_argument("--read-csv", dest="read_csv", metavar="FILE",
|
||||
parser.add_option("--read-csv", dest="read_csv", metavar="FILE",
|
||||
help="Read parameters from CSV file rather than command line")
|
||||
|
||||
parser.add_argument("--write-csv", dest="write_csv", metavar="FILE",
|
||||
parser.add_option("--write-csv", dest="write_csv", metavar="FILE",
|
||||
help="Append generated parameters in CSV file",
|
||||
)
|
||||
parser.add_argument("--write-hlr", dest="write_hlr", metavar="FILE",
|
||||
parser.add_option("--write-hlr", dest="write_hlr", metavar="FILE",
|
||||
help="Append generated parameters to OpenBSC HLR sqlite3",
|
||||
)
|
||||
parser.add_argument("--dry-run", dest="dry_run",
|
||||
parser.add_option("--dry-run", dest="dry_run",
|
||||
help="Perform a 'dry run', don't actually program the card",
|
||||
default=False, action="store_true")
|
||||
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
|
||||
parser.add_option("--card_handler", dest="card_handler_config", metavar="FILE",
|
||||
help="Use automatic card handling machine")
|
||||
|
||||
options = parser.parse_args()
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if options.type == 'list':
|
||||
for kls in _cards_classes:
|
||||
@@ -197,13 +219,15 @@ def parse_options():
|
||||
return options
|
||||
|
||||
if options.source == 'csv':
|
||||
if (options.imsi is None) and (options.iccid is None) and (options.read_imsi is False) and (options.read_iccid is False):
|
||||
parser.error("CSV mode requires one additional parameter: --read-iccid, --read-imsi, --iccid or --imsi")
|
||||
if (options.imsi is None) and (options.batch_mode is False) and (options.read_imsi is False) and (options.read_iccid is False):
|
||||
parser.error(
|
||||
"CSV mode needs either an IMSI, --read-imsi, --read-iccid or batch mode")
|
||||
if options.read_csv is None:
|
||||
parser.error("CSV mode requires a CSV input file")
|
||||
elif options.source == 'cmdline':
|
||||
if ((options.imsi is None) or (options.iccid is None)) and (options.num is None):
|
||||
parser.error("If either IMSI or ICCID isn't specified, num is required")
|
||||
parser.error(
|
||||
"If either IMSI or ICCID isn't specified, num is required")
|
||||
else:
|
||||
parser.error("Only `cmdline' and `csv' sources supported")
|
||||
|
||||
@@ -218,6 +242,9 @@ def parse_options():
|
||||
parser.error(
|
||||
"Can't give ICCID/IMSI for batch mode, need to use automatic parameters ! see --num and --secret for more information")
|
||||
|
||||
if args:
|
||||
parser.error("Extraneous arguments")
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@@ -622,9 +649,6 @@ def read_params_csv(opts, imsi=None, iccid=None):
|
||||
|
||||
def write_params_hlr(opts, params):
|
||||
# SQLite3 OpenBSC HLR
|
||||
# FIXME: The format of the osmo-hlr database has evolved, so that the code below will no longer work.
|
||||
print("Warning: the database format of recent OsmoHLR versions is not compatible with pySim-prog!")
|
||||
|
||||
if opts.write_hlr:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(opts.write_hlr)
|
||||
@@ -725,18 +749,16 @@ def process_card(scc, opts, first, ch):
|
||||
card.erase()
|
||||
card.reset()
|
||||
|
||||
cp = None
|
||||
|
||||
# Generate parameters
|
||||
if opts.source == 'cmdline':
|
||||
cp = gen_parameters(opts)
|
||||
elif opts.source == 'csv':
|
||||
imsi = None
|
||||
iccid = None
|
||||
if opts.read_iccid:
|
||||
(res, _) = scc.read_binary(['3f00', '2fe2'], length=10)
|
||||
iccid = dec_iccid(res)
|
||||
else:
|
||||
iccid = opts.iccid
|
||||
if opts.read_imsi:
|
||||
elif opts.read_imsi:
|
||||
(res, _) = scc.read_binary(EF['IMSI'])
|
||||
imsi = swap_nibbles(res)[3:]
|
||||
else:
|
||||
|
||||
@@ -29,8 +29,6 @@ import random
|
||||
import re
|
||||
import sys
|
||||
|
||||
from osmocom.utils import h2b, h2s, swap_nibbles, rpad
|
||||
|
||||
from pySim.ts_51_011 import EF_SST_map, EF_AD
|
||||
from pySim.legacy.ts_51_011 import EF, DF
|
||||
from pySim.ts_31_102 import EF_UST_map
|
||||
@@ -42,8 +40,8 @@ from pySim.commands import SimCardCommands
|
||||
from pySim.transport import init_reader, argparse_add_reader_args
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.legacy.cards import card_detect, SimCard, UsimCard, IsimCard
|
||||
from pySim.utils import dec_imsi, dec_iccid
|
||||
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
|
||||
from pySim.utils import h2b, h2s, swap_nibbles, rpad, dec_imsi, dec_iccid, dec_msisdn
|
||||
from pySim.legacy.utils import format_xplmn_w_act, dec_st
|
||||
|
||||
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
@@ -88,7 +86,7 @@ if __name__ == '__main__':
|
||||
scc.sel_ctrl = "0004"
|
||||
|
||||
# Testing for Classic SIM or UICC
|
||||
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00" + "00")
|
||||
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00")
|
||||
if sw == '6e00':
|
||||
# Just a Classic SIM
|
||||
scc.cla_byte = "a0"
|
||||
|
||||
625
pySim-shell.py
625
pySim-shell.py
@@ -21,7 +21,6 @@ from typing import List, Optional
|
||||
|
||||
import json
|
||||
import traceback
|
||||
import re
|
||||
|
||||
import cmd2
|
||||
from packaging import version
|
||||
@@ -48,17 +47,14 @@ from io import StringIO
|
||||
|
||||
from pprint import pprint as pp
|
||||
|
||||
from osmocom.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, is_hexstr, is_decimal
|
||||
from osmocom.utils import is_hexstr_or_decimal, Hexstr
|
||||
from osmocom.tlv import bertlv_parse_one
|
||||
|
||||
from pySim.exceptions import *
|
||||
from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
|
||||
from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, dec_iccid, sw_match
|
||||
from pySim.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, bertlv_parse_one, sw_match
|
||||
from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr, dec_iccid
|
||||
from pySim.utils import is_hexstr_or_decimal, is_hexstr, is_decimal
|
||||
from pySim.card_handler import CardHandler, CardHandlerAuto
|
||||
|
||||
from pySim.filesystem import CardMF, CardEF, CardDF, CardADF, LinFixedEF, TransparentEF, BerTlvEF
|
||||
from pySim.ts_102_221 import pin_names
|
||||
from pySim.filesystem import CardMF, CardDF, CardADF
|
||||
from pySim.ts_102_222 import Ts102222Commands
|
||||
from pySim.gsm_r import DF_EIRENE
|
||||
from pySim.cat import ProactiveCommand
|
||||
@@ -114,7 +110,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.conserve_write = True
|
||||
self.json_pretty_print = True
|
||||
self.apdu_trace = False
|
||||
self.apdu_strict = False
|
||||
|
||||
self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
|
||||
onchange_cb=self._onchange_numeric_path))
|
||||
@@ -123,9 +118,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
|
||||
self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
|
||||
onchange_cb=self._onchange_apdu_trace))
|
||||
self.add_settable(Settable2Compat('apdu_strict', bool,
|
||||
'Enforce APDU responses according to ISO/IEC 7816-3, table 12', self,
|
||||
onchange_cb=self._onchange_apdu_strict))
|
||||
self.equip(card, rs)
|
||||
|
||||
def equip(self, card, rs):
|
||||
@@ -155,7 +147,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
# When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
|
||||
# needed to operate on cards.
|
||||
if self.card and self.rs:
|
||||
self.rs.reset()
|
||||
self.lchan = self.rs.lchan[0]
|
||||
self._onchange_conserve_write(
|
||||
'conserve_write', False, self.conserve_write)
|
||||
@@ -169,9 +160,9 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
|
||||
try:
|
||||
self.lchan.select('MF/EF.ICCID', self)
|
||||
rs.identity['ICCID'] = dec_iccid(self.lchan.read_binary()[0])
|
||||
self.iccid = dec_iccid(self.lchan.read_binary()[0])
|
||||
except:
|
||||
rs.identity['ICCID'] = None
|
||||
self.iccid = None
|
||||
|
||||
self.lchan.select('MF', self)
|
||||
rc = True
|
||||
@@ -203,13 +194,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
else:
|
||||
self.card._scc._tp.apdu_tracer = None
|
||||
|
||||
def _onchange_apdu_strict(self, param_name, old, new):
|
||||
if self.card:
|
||||
if new == True:
|
||||
self.card._scc._tp.apdu_strict = True
|
||||
else:
|
||||
self.card._scc._tp.apdu_strict = False
|
||||
|
||||
class Cmd2ApduTracer(ApduTracer):
|
||||
def __init__(self, cmd2_app):
|
||||
self.cmd2 = cmd2_app
|
||||
@@ -219,23 +203,14 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.cmd2.poutput("<- %s: %s" % (sw, resp))
|
||||
|
||||
def update_prompt(self):
|
||||
if self.rs and self.rs.adm_verified:
|
||||
prompt_char = '#'
|
||||
else:
|
||||
prompt_char = '>'
|
||||
|
||||
if self.lchan:
|
||||
path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
|
||||
scp = self.lchan.scc.scp
|
||||
if scp:
|
||||
self.prompt = 'pySIM-shell (%s:%02u:%s)%c ' % (str(scp), self.lchan.lchan_nr, path_str, prompt_char)
|
||||
else:
|
||||
self.prompt = 'pySIM-shell (%02u:%s)%c ' % (self.lchan.lchan_nr, path_str, prompt_char)
|
||||
self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, path_str)
|
||||
else:
|
||||
if self.card:
|
||||
self.prompt = 'pySIM-shell (no card profile)%c ' % prompt_char
|
||||
self.prompt = 'pySIM-shell (no card profile)> '
|
||||
else:
|
||||
self.prompt = 'pySIM-shell (no card)%c ' % prompt_char
|
||||
self.prompt = 'pySIM-shell (no card)> '
|
||||
|
||||
@cmd2.with_category(CUSTOM_CATEGORY)
|
||||
def do_intro(self, _):
|
||||
@@ -256,10 +231,8 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.equip(card, rs)
|
||||
|
||||
apdu_cmd_parser = argparse.ArgumentParser()
|
||||
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
|
||||
apdu_cmd_parser.add_argument('--expect-response-regex', help='match response against regex', type=str, default=None)
|
||||
apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
|
||||
apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string')
|
||||
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
|
||||
|
||||
@cmd2.with_argparser(apdu_cmd_parser)
|
||||
def do_apdu(self, opts):
|
||||
@@ -272,10 +245,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
# noted that the apdu command plays an exceptional role since it is the only card accessing command that
|
||||
# can be executed without the presence of a runtime state (self.rs) object. However, this also means that
|
||||
# self.lchan is also not present (see method equip).
|
||||
if opts.raw or self.lchan is None:
|
||||
data, sw = self.card._scc.send_apdu(opts.APDU, apply_lchan = False)
|
||||
else:
|
||||
data, sw = self.lchan.scc.send_apdu(opts.APDU, apply_lchan = False)
|
||||
data, sw = self.card._scc._tp.send_apdu(opts.APDU)
|
||||
if data:
|
||||
self.poutput("SW: %s, RESP: %s" % (sw, data))
|
||||
else:
|
||||
@@ -283,22 +253,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
if opts.expect_sw:
|
||||
if not sw_match(sw, opts.expect_sw):
|
||||
raise SwMatchError(sw, opts.expect_sw)
|
||||
if opts.expect_response_regex:
|
||||
response_regex_compiled = re.compile(opts.expect_response_regex)
|
||||
if re.match(response_regex_compiled, data) is None:
|
||||
raise ValueError("RESP does not match regex \'%s\'" % opts.expect_response_regex)
|
||||
|
||||
@cmd2.with_category(CUSTOM_CATEGORY)
|
||||
def do_reset(self, opts):
|
||||
"""Reset the Card."""
|
||||
if self.rs is None:
|
||||
# In case no runtime state is available we go the direct route
|
||||
self.card._scc.reset_card()
|
||||
atr = self.card._scc.get_atr()
|
||||
else:
|
||||
atr = self.rs.reset(self)
|
||||
self.poutput('Card ATR: %s' % atr)
|
||||
self.update_prompt()
|
||||
|
||||
class InterceptStderr(list):
|
||||
def __init__(self):
|
||||
@@ -376,7 +330,8 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
return -1
|
||||
|
||||
bulk_script_parser = argparse.ArgumentParser()
|
||||
bulk_script_parser.add_argument('SCRIPT_PATH', help="path to the script file")
|
||||
bulk_script_parser.add_argument(
|
||||
'script_path', help="path to the script file")
|
||||
bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
|
||||
action='store_true')
|
||||
bulk_script_parser.add_argument('--tries', type=int, default=2,
|
||||
@@ -392,7 +347,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
"""Run script on multiple cards (bulk provisioning)"""
|
||||
|
||||
# Make sure that the script file exists and that it is readable.
|
||||
if not os.access(opts.SCRIPT_PATH, os.R_OK):
|
||||
if not os.access(opts.script_path, os.R_OK):
|
||||
self.poutput("Invalid script file!")
|
||||
return
|
||||
|
||||
@@ -402,7 +357,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
first = True
|
||||
while 1:
|
||||
# TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
|
||||
# The rationale is: There may be a problem with the device, we do want to prevent that
|
||||
# The ratinale is: There may be a problem with the device, we do want to prevent that
|
||||
# all remaining cards are fired to the error bin. This is only relevant for situations
|
||||
# with large stacks, probably we do not need this feature right now.
|
||||
|
||||
@@ -417,7 +372,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
os.system(opts.pre_card_action)
|
||||
|
||||
# process the card
|
||||
rc = self._process_card(first, opts.SCRIPT_PATH)
|
||||
rc = self._process_card(first, opts.script_path)
|
||||
if rc == 0:
|
||||
success_count = success_count + 1
|
||||
self._show_success_sign()
|
||||
@@ -469,13 +424,13 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
first = False
|
||||
|
||||
echo_parser = argparse.ArgumentParser()
|
||||
echo_parser.add_argument('STRING', help="string to echo on the shell", nargs='+')
|
||||
echo_parser.add_argument('string', help="string to echo on the shell", nargs='+')
|
||||
|
||||
@cmd2.with_argparser(echo_parser)
|
||||
@cmd2.with_category(CUSTOM_CATEGORY)
|
||||
def do_echo(self, opts):
|
||||
"""Echo (print) a string on the console"""
|
||||
self.poutput(' '.join(opts.STRING))
|
||||
self.poutput(' '.join(opts.string))
|
||||
|
||||
@cmd2.with_category(CUSTOM_CATEGORY)
|
||||
def do_version(self, opts):
|
||||
@@ -524,25 +479,12 @@ class PySimCommands(CommandSet):
|
||||
self._cmd.poutput(directory_str)
|
||||
self._cmd.poutput("%d files" % len(selectables))
|
||||
|
||||
def __walk_action(self, action, filename, context, **kwargs):
|
||||
# Changing the currently selected file while walking over the filesystem tree would disturb the
|
||||
# walk, so we memorize the currently selected file here so that we can select it again after
|
||||
# we have executed the action callback.
|
||||
selected_file_before_action = self._cmd.lchan.selected_file
|
||||
|
||||
# Perform action
|
||||
action(filename, context, **kwargs)
|
||||
|
||||
# When the action callback is done, make sure the file that was selected before is selected again.
|
||||
if selected_file_before_action != self._cmd.lchan.selected_file:
|
||||
self._cmd.lchan.select_file(selected_file_before_action, self._cmd)
|
||||
|
||||
def __walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
|
||||
def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
|
||||
"""Recursively walk through the file system, starting at the currently selected DF"""
|
||||
|
||||
if isinstance(self._cmd.lchan.selected_file, CardDF):
|
||||
if action_df:
|
||||
self.__walk_action(action_df, self._cmd.lchan.selected_file.name, context, **kwargs)
|
||||
action_df(context, **kwargs)
|
||||
|
||||
files = self._cmd.lchan.selected_file.get_selectables(
|
||||
flags=['FNAMES', 'ANAMES'])
|
||||
@@ -575,45 +517,143 @@ class PySimCommands(CommandSet):
|
||||
# If the DF was skipped, we never have entered the directory
|
||||
# below, so we must not move up.
|
||||
if skip_df == False:
|
||||
self.__walk(indent + 1, action_ef, action_df, context, **kwargs)
|
||||
self._cmd.lchan.select_file(self._cmd.lchan.selected_file.parent, self._cmd)
|
||||
self.walk(indent + 1, action_ef, action_df, context, **kwargs)
|
||||
|
||||
parent = self._cmd.lchan.selected_file.parent
|
||||
df = self._cmd.lchan.selected_file
|
||||
adf = self._cmd.lchan.selected_adf
|
||||
if isinstance(parent, CardMF) and (adf and adf.has_fs == False):
|
||||
# Not every application that may be present on a GlobalPlatform card will support the SELECT
|
||||
# command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
|
||||
# SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
|
||||
# Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
|
||||
# "select by name" method, which means we can only select an application and not a file.
|
||||
# The consequence of this is that we may get trapped in an application that does not have
|
||||
# ISIM/USIM like file system support and the only way to leave that application is to select
|
||||
# an ISIM/USIM application in order to get the file system access back.
|
||||
#
|
||||
# To automate this escape-route while traversing the file system we will check whether
|
||||
# the parent file is the MF. When this is the case and the selected ADF has no file system
|
||||
# support, we will select an arbitrary ADF that has file system support first and from there
|
||||
# we will then select the MF.
|
||||
for selectable in parent.get_selectables().items():
|
||||
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
|
||||
self._cmd.lchan.select(selectable[1].name, self._cmd)
|
||||
break
|
||||
self._cmd.lchan.select(df.get_mf().name, self._cmd)
|
||||
else:
|
||||
# Normal DF/ADF selection
|
||||
fcp_dec = self._cmd.lchan.select("..", self._cmd)
|
||||
|
||||
elif action_ef:
|
||||
self.__walk_action(action_ef, f, context, **kwargs)
|
||||
df_before_action = self._cmd.lchan.selected_file
|
||||
action_ef(f, context, **kwargs)
|
||||
# When walking through the file system tree the action must not
|
||||
# always restore the currently selected file to the file that
|
||||
# was selected before executing the action() callback.
|
||||
if df_before_action != self._cmd.lchan.selected_file:
|
||||
raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
|
||||
% (str(self._cmd.lchan.selected_file), str(df_before_action)))
|
||||
|
||||
def do_tree(self, opts):
|
||||
"""Display a filesystem-tree with all selectable files"""
|
||||
self.__walk()
|
||||
self.walk()
|
||||
|
||||
def __export_file(self, filename, context, as_json):
|
||||
""" Select and export a single file (EF, DF or ADF) """
|
||||
def export_ef(self, filename, context, as_json):
|
||||
""" Select and export a single elementary file (EF) """
|
||||
context['COUNT'] += 1
|
||||
df = self._cmd.lchan.selected_file
|
||||
|
||||
file = self._cmd.lchan.get_file_by_name(filename)
|
||||
if file:
|
||||
self._cmd.poutput(boxed_heading_str(file.fully_qualified_path_str(True)))
|
||||
self._cmd.poutput("# directory: %s (%s)" % (file.fully_qualified_path_str(True),
|
||||
file.fully_qualified_path_str(False)))
|
||||
else:
|
||||
# If this is called from self.__walk(), then it is ensured that the file exists.
|
||||
raise RuntimeError("cannot export, file %s does not exist in the file system tree" % filename)
|
||||
# The currently selected file (not the file we are going to export)
|
||||
# must always be an ADF or DF. From this starting point we select
|
||||
# the EF we want to export. To maintain consistency we will then
|
||||
# select the current DF again (see comment below).
|
||||
if not isinstance(df, CardDF):
|
||||
raise RuntimeError(
|
||||
"currently selected file %s is not a DF or ADF" % str(df))
|
||||
|
||||
df_path_list = df.fully_qualified_path(True)
|
||||
df_path = df.fully_qualified_path_str(True)
|
||||
df_path_fid = df.fully_qualified_path_str(False)
|
||||
|
||||
file_str = df_path + "/" + str(filename)
|
||||
self._cmd.poutput(boxed_heading_str(file_str))
|
||||
|
||||
self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
|
||||
try:
|
||||
fcp_dec = self._cmd.lchan.select_file(file, self._cmd)
|
||||
self._cmd.poutput("# file: %s (%s)" %
|
||||
(self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
|
||||
if isinstance(self._cmd.lchan.selected_file, CardEF):
|
||||
self._cmd.poutput("# structure: %s" % str(self._cmd.lchan.selected_file_structure()))
|
||||
fcp_dec = self._cmd.lchan.select(filename, self._cmd)
|
||||
self._cmd.poutput("# file: %s (%s)" % (
|
||||
self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
|
||||
|
||||
structure = self._cmd.lchan.selected_file_structure()
|
||||
self._cmd.poutput("# structure: %s" % str(structure))
|
||||
self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
|
||||
self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
|
||||
self._cmd.poutput("select " + self._cmd.lchan.selected_file.fully_qualified_path_str())
|
||||
self._cmd.poutput(self._cmd.lchan.selected_file.export(as_json, self._cmd.lchan))
|
||||
|
||||
for f in df_path_list:
|
||||
self._cmd.poutput("select " + str(f))
|
||||
self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
|
||||
|
||||
if structure == 'transparent':
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_binary_dec()
|
||||
self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
|
||||
else:
|
||||
result = self._cmd.lchan.read_binary()
|
||||
self._cmd.poutput("update_binary " + str(result[0]))
|
||||
elif structure == 'cyclic' or structure == 'linear_fixed':
|
||||
# Use number of records specified in select response
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
if num_of_rec:
|
||||
for r in range(1, num_of_rec + 1):
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_record_dec(r)
|
||||
self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
|
||||
else:
|
||||
result = self._cmd.lchan.read_record(r)
|
||||
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
|
||||
|
||||
# When the select response does not return the number of records, read until we hit the
|
||||
# first record that cannot be read.
|
||||
else:
|
||||
r = 1
|
||||
while True:
|
||||
try:
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_record_dec(r)
|
||||
self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
|
||||
else:
|
||||
result = self._cmd.lchan.read_record(r)
|
||||
self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
|
||||
except SwMatchError as e:
|
||||
# We are past the last valid record - stop
|
||||
if e.sw_actual == "9402":
|
||||
break
|
||||
# Some other problem occurred
|
||||
else:
|
||||
raise e
|
||||
r = r + 1
|
||||
elif structure == 'ber_tlv':
|
||||
tags = self._cmd.lchan.retrieve_tags()
|
||||
for t in tags:
|
||||
result = self._cmd.lchan.retrieve_data(t)
|
||||
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
|
||||
self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'Unsupported structure "%s" of file "%s"' % (structure, filename))
|
||||
except Exception as e:
|
||||
bad_file_str = file.fully_qualified_path_str(True) + "/" + str(file.name) + ", " + str(e)
|
||||
bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
|
||||
self._cmd.poutput("# bad file: %s" % bad_file_str)
|
||||
context['ERR'] += 1
|
||||
context['BAD'].append(bad_file_str)
|
||||
|
||||
# When reading the file is done, make sure the parent file is
|
||||
# selected again. This will be the usual case, however we need
|
||||
# to check before since we must not select the same DF twice
|
||||
if df != self._cmd.lchan.selected_file:
|
||||
self._cmd.lchan.select(df.fid or df.aid, self._cmd)
|
||||
|
||||
self._cmd.poutput("#")
|
||||
|
||||
export_parser = argparse.ArgumentParser()
|
||||
@@ -631,10 +671,10 @@ class PySimCommands(CommandSet):
|
||||
exception_str_add = ""
|
||||
|
||||
if opts.filename:
|
||||
self.__walk_action(self.__export_file, opts.filename, context, **kwargs_export)
|
||||
self.export_ef(opts.filename, context, **kwargs_export)
|
||||
else:
|
||||
try:
|
||||
self.__walk(0, self.__export_file, self.__export_file, context, **kwargs_export)
|
||||
self.walk(0, self.export_ef, None, context, **kwargs_export)
|
||||
except Exception as e:
|
||||
print("# Stopping early here due to exception: " + str(e))
|
||||
print("#")
|
||||
@@ -662,222 +702,61 @@ class PySimCommands(CommandSet):
|
||||
raise RuntimeError(
|
||||
"unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
|
||||
|
||||
def __dump_file(self, filename, context, as_json):
|
||||
""" Select and dump a single file (EF, DF or ADF) """
|
||||
file = self._cmd.lchan.get_file_by_name(filename)
|
||||
if file:
|
||||
res = {
|
||||
'path': file.fully_qualified_path(True)
|
||||
}
|
||||
else:
|
||||
# If this is called from self.__walk(), then it is ensured that the file exists.
|
||||
raise RuntimeError("cannot dump, file %s does not exist in the file system tree" % filename)
|
||||
|
||||
try:
|
||||
fcp_dec = self._cmd.lchan.select(filename, self._cmd)
|
||||
|
||||
# File control parameters (common for EF, DF and ADF files)
|
||||
if not self._cmd.lchan.selected_file_fcp_hex:
|
||||
# An application without a real ADF (like ADF.ARA-M) / filesystem
|
||||
return
|
||||
|
||||
res['fcp_raw'] = str(self._cmd.lchan.selected_file_fcp_hex)
|
||||
res['fcp'] = fcp_dec
|
||||
|
||||
# File structure and contents (EF only)
|
||||
if isinstance(self._cmd.lchan.selected_file, CardEF):
|
||||
structure = self._cmd.lchan.selected_file_structure()
|
||||
if structure == 'transparent':
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_binary_dec()
|
||||
body = result[0]
|
||||
else:
|
||||
result = self._cmd.lchan.read_binary()
|
||||
body = str(result[0])
|
||||
elif structure == 'cyclic' or structure == 'linear_fixed':
|
||||
body = []
|
||||
# Use number of records specified in select response
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
if num_of_rec:
|
||||
for r in range(1, num_of_rec + 1):
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_record_dec(r)
|
||||
body.append(result[0])
|
||||
else:
|
||||
result = self._cmd.lchan.read_record(r)
|
||||
body.append(str(result[0]))
|
||||
|
||||
# When the select response does not return the number of records, read until we hit the
|
||||
# first record that cannot be read.
|
||||
else:
|
||||
r = 1
|
||||
while True:
|
||||
try:
|
||||
if as_json:
|
||||
result = self._cmd.lchan.read_record_dec(r)
|
||||
body.append(result[0])
|
||||
else:
|
||||
result = self._cmd.lchan.read_record(r)
|
||||
body.append(str(result[0]))
|
||||
except SwMatchError as e:
|
||||
# We are past the last valid record - stop
|
||||
if e.sw_actual == "9402":
|
||||
break
|
||||
# Some other problem occurred
|
||||
raise e
|
||||
r = r + 1
|
||||
elif structure == 'ber_tlv':
|
||||
tags = self._cmd.lchan.retrieve_tags()
|
||||
body = {}
|
||||
for t in tags:
|
||||
result = self._cmd.lchan.retrieve_data(t)
|
||||
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
|
||||
body[t] = b2h(val)
|
||||
else:
|
||||
raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
|
||||
res['body'] = body
|
||||
|
||||
except SwMatchError as e:
|
||||
res['error'] = {
|
||||
'sw_actual': e.sw_actual,
|
||||
'sw_expected': e.sw_expected,
|
||||
'message': e.description,
|
||||
}
|
||||
except Exception as e:
|
||||
raise(e)
|
||||
res['error'] = {
|
||||
'message': str(e)
|
||||
}
|
||||
|
||||
context['result']['files'][file.fully_qualified_path_str(True)] = res
|
||||
|
||||
fsdump_parser = argparse.ArgumentParser()
|
||||
fsdump_parser.add_argument(
|
||||
'--filename', type=str, default=None, help='only export specific (named) file')
|
||||
fsdump_parser.add_argument(
|
||||
'--json', action='store_true', help='export file contents as JSON (less reliable)')
|
||||
|
||||
@cmd2.with_argparser(fsdump_parser)
|
||||
def do_fsdump(self, opts):
|
||||
"""Export filesystem metadata and file contents of all files below current DF in
|
||||
machine-readable json format. This is similar to "export", but much easier to parse by
|
||||
downstream processing tools. You usually may want to call this from the MF and verify
|
||||
the ADM1 PIN (if available) to maximize the amount of readable files."""
|
||||
result = {
|
||||
'name': self._cmd.card.name,
|
||||
'atr': self._cmd.rs.identity['ATR'],
|
||||
'eid': self._cmd.rs.identity.get('EID', None),
|
||||
'iccid': self._cmd.rs.identity.get('ICCID', None),
|
||||
'aids': {x.aid:{} for x in self._cmd.rs.mf.applications.values()},
|
||||
'files': {},
|
||||
}
|
||||
context = {'result': result, 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
|
||||
kwargs_export = {'as_json': opts.json}
|
||||
exception_str_add = ""
|
||||
|
||||
if opts.filename:
|
||||
self.__walk_action(self.__dump_file, opts.filename, context, **kwargs_export)
|
||||
else:
|
||||
# export an entire subtree
|
||||
try:
|
||||
self.__walk(0, self.__dump_file, self.__dump_file, context, **kwargs_export)
|
||||
except Exception as e:
|
||||
print("# Stopping early here due to exception: " + str(e))
|
||||
print("#")
|
||||
exception_str_add = ", also had to stop early due to exception:" + str(e)
|
||||
#raise e
|
||||
|
||||
self._cmd.poutput_json(context['result'])
|
||||
|
||||
def do_reset(self, opts):
|
||||
"""Reset the Card."""
|
||||
atr = self._cmd.card.reset()
|
||||
self._cmd.poutput('Card ATR: %s' % i2h(atr))
|
||||
self._cmd.update_prompt()
|
||||
|
||||
def do_desc(self, opts):
|
||||
"""Display human readable file description for the currently selected file"""
|
||||
desc = self._cmd.lchan.selected_file.desc
|
||||
if desc:
|
||||
self._cmd.poutput("%s: %s" % (self._cmd.lchan.selected_file, desc))
|
||||
self._cmd.poutput(desc)
|
||||
else:
|
||||
self._cmd.poutput("%s: no description available" % self._cmd.lchan.selected_file)
|
||||
self._cmd.poutput(" file structure: %s" % self._cmd.lchan.selected_file_structure())
|
||||
if isinstance(self._cmd.lchan.selected_file, LinFixedEF):
|
||||
self._cmd.poutput(" record length:")
|
||||
self._cmd.poutput(" minimum_length: %s" % str(self._cmd.lchan.selected_file.rec_len[0]))
|
||||
self._cmd.poutput(" recommended_length: %s" % str(self._cmd.lchan.selected_file.rec_len[1]))
|
||||
self._cmd.poutput(" actual_length: %s" % str(self._cmd.lchan.selected_file_record_len()))
|
||||
self._cmd.poutput(" number of records: %s" % str(self._cmd.lchan.selected_file_num_of_rec()))
|
||||
elif isinstance(self._cmd.lchan.selected_file, TransparentEF):
|
||||
self._cmd.poutput(" file size:")
|
||||
self._cmd.poutput(" minimum_size: %s" % str(self._cmd.lchan.selected_file.size[0]))
|
||||
self._cmd.poutput(" recommended_size: %s" % str(self._cmd.lchan.selected_file.size[1]))
|
||||
self._cmd.poutput(" actual_size: %s" % str(self._cmd.lchan.selected_file_size()))
|
||||
elif isinstance(self._cmd.lchan.selected_file, BerTlvEF):
|
||||
self._cmd.poutput(" file size:")
|
||||
self._cmd.poutput(" minimum_size: %s" % str(self._cmd.lchan.selected_file.size[0]))
|
||||
self._cmd.poutput(" recommended_size: %s" % str(self._cmd.lchan.selected_file.size[1]))
|
||||
self._cmd.poutput(" actual_size: %s" % str(self._cmd.lchan.selected_file_size()))
|
||||
self._cmd.poutput(" reserved_file_size: %s" % str(self._cmd.lchan.selected_file_reserved_file_size()))
|
||||
self._cmd.poutput(" maximum_file_size: %s" % str(self._cmd.lchan.selected_file_maximum_file_size()))
|
||||
self._cmd.poutput("no description available")
|
||||
|
||||
verify_adm_parser = argparse.ArgumentParser()
|
||||
verify_adm_parser.add_argument('--pin-is-hex', action='store_true',
|
||||
help='ADM pin value is specified as hex-string (not decimal)')
|
||||
verify_adm_parser.add_argument('--adm-type',
|
||||
choices=[x for x in pin_names.values() if x.startswith('ADM')],
|
||||
help='Override ADM number. Default is card-model-specific, usually 1')
|
||||
verify_adm_parser.add_argument('ADM', nargs='?', type=is_hexstr_or_decimal,
|
||||
help='ADM pin value. If none given, CSV file will be queried')
|
||||
verify_adm_parser.add_argument('ADM1', nargs='?', type=is_hexstr_or_decimal,
|
||||
help='ADM1 pin value. If none given, CSV file will be queried')
|
||||
|
||||
@cmd2.with_argparser(verify_adm_parser)
|
||||
def do_verify_adm(self, opts):
|
||||
"""Verify the ADM (Administrator) PIN specified as argument. This is typically needed in order
|
||||
to get write/update permissions to most of the files on SIM cards.
|
||||
"""
|
||||
if opts.adm_type:
|
||||
# pylint: disable=unsubscriptable-object
|
||||
adm_chv_num = pin_names.inverse[opts.adm_type]
|
||||
else:
|
||||
adm_chv_num = self._cmd.card._adm_chv_num
|
||||
if opts.ADM:
|
||||
to get write/update permissions to most of the files on SIM cards.
|
||||
|
||||
Currently only ADM1 is supported."""
|
||||
if opts.ADM1:
|
||||
# use specified ADM-PIN
|
||||
if opts.pin_is_hex:
|
||||
pin_adm = sanitize_pin_adm(None, opts.ADM)
|
||||
else:
|
||||
pin_adm = sanitize_pin_adm(opts.ADM)
|
||||
pin_adm = sanitize_pin_adm(opts.ADM1)
|
||||
else:
|
||||
iccid = self._cmd.rs.identity['ICCID']
|
||||
adm_type = opts.adm_type or 'ADM1'
|
||||
# try to find an ADM-PIN if none is specified
|
||||
result = card_key_provider_get_field(adm_type, key='ICCID', value=iccid)
|
||||
if opts.pin_is_hex or (result and len(result) > 8):
|
||||
pin_adm = sanitize_pin_adm(None, result)
|
||||
else:
|
||||
pin_adm = sanitize_pin_adm(result)
|
||||
result = card_key_provider_get_field(
|
||||
'ADM1', key='ICCID', value=self._cmd.iccid)
|
||||
pin_adm = sanitize_pin_adm(result)
|
||||
if pin_adm:
|
||||
self._cmd.poutput("found %s '%s' for ICCID '%s'" % (adm_type, result, iccid))
|
||||
self._cmd.poutput(
|
||||
"found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
|
||||
else:
|
||||
raise ValueError("cannot find %s for ICCID '%s'" % (adm_type, iccid))
|
||||
raise ValueError(
|
||||
"cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
|
||||
|
||||
if pin_adm:
|
||||
self._cmd.lchan.scc.verify_chv(adm_chv_num, h2b(pin_adm))
|
||||
self._cmd.lchan.scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
|
||||
else:
|
||||
raise ValueError("error: cannot authenticate, no adm-pin!")
|
||||
self._cmd.rs.adm_verified = True
|
||||
self._cmd.update_prompt()
|
||||
|
||||
def do_cardinfo(self, opts):
|
||||
"""Display information about the currently inserted card"""
|
||||
self._cmd.poutput("Card info:")
|
||||
self._cmd.poutput(" Name: %s" % self._cmd.card.name)
|
||||
self._cmd.poutput(" ATR: %s" % self._cmd.rs.identity['ATR'].lower())
|
||||
eid = self._cmd.rs.identity.get('EID', None)
|
||||
if eid:
|
||||
self._cmd.poutput(" EID: %s" % eid.lower())
|
||||
self._cmd.poutput(" ICCID: %s" % self._cmd.rs.identity['ICCID'].lower())
|
||||
self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte.lower())
|
||||
self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl.lower())
|
||||
if len(self._cmd.rs.mf.applications) > 0:
|
||||
self._cmd.poutput(" AIDs:")
|
||||
for a in self._cmd.rs.mf.applications:
|
||||
self._cmd.poutput(" %s" % a.lower())
|
||||
self._cmd.poutput(" ATR: %s" % b2h(self._cmd.lchan.scc.get_atr()))
|
||||
self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
|
||||
self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte)
|
||||
self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl)
|
||||
self._cmd.poutput(" AIDs:")
|
||||
for a in self._cmd.rs.mf.applications:
|
||||
self._cmd.poutput(" %s" % a)
|
||||
|
||||
@with_default_category('ISO7816 Commands')
|
||||
class Iso7816Commands(CommandSet):
|
||||
@@ -902,64 +781,69 @@ class Iso7816Commands(CommandSet):
|
||||
index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
|
||||
return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
|
||||
|
||||
def get_code(self, code, field):
|
||||
"""Use code either directly or try to get it from external data source using the provided field name"""
|
||||
if code is not None:
|
||||
def get_code(self, code):
|
||||
"""Use code either directly or try to get it from external data source"""
|
||||
auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
|
||||
|
||||
if str(code).upper() not in auto:
|
||||
return sanitize_pin_adm(code)
|
||||
iccid = self._cmd.rs.identity['ICCID']
|
||||
result = card_key_provider_get_field(field, key='ICCID', value=iccid)
|
||||
|
||||
result = card_key_provider_get_field(
|
||||
str(code), key='ICCID', value=self._cmd.iccid)
|
||||
result = sanitize_pin_adm(result)
|
||||
if result:
|
||||
self._cmd.poutput("found %s '%s' for ICCID '%s'" % (field, result, iccid))
|
||||
self._cmd.poutput("found %s '%s' for ICCID '%s'" %
|
||||
(code.upper(), result, self._cmd.iccid))
|
||||
else:
|
||||
raise RuntimeError("cannot find %s for ICCID '%s'" % (field, iccid))
|
||||
self._cmd.poutput("cannot find %s for ICCID '%s'" %
|
||||
(code.upper(), self._cmd.iccid))
|
||||
return result
|
||||
|
||||
verify_chv_parser = argparse.ArgumentParser()
|
||||
verify_chv_parser.add_argument(
|
||||
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
|
||||
verify_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
verify_chv_parser.add_argument(
|
||||
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
|
||||
@cmd2.with_argparser(verify_chv_parser)
|
||||
def do_verify_chv(self, opts):
|
||||
"""Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
|
||||
call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
|
||||
PIN2."""
|
||||
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
|
||||
pin = self.get_code(opts.pin_code)
|
||||
(data, sw) = self._cmd.lchan.scc.verify_chv(opts.pin_nr, h2b(pin))
|
||||
self._cmd.poutput("CHV verification successful")
|
||||
|
||||
unblock_chv_parser = argparse.ArgumentParser()
|
||||
unblock_chv_parser.add_argument(
|
||||
'--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
|
||||
unblock_chv_parser.add_argument('PUK', nargs='?', type=is_decimal,
|
||||
help='PUK code value. If none given, CSV file will be queried')
|
||||
unblock_chv_parser.add_argument('NEWPIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
unblock_chv_parser.add_argument(
|
||||
'puk_code', type=is_decimal, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
|
||||
unblock_chv_parser.add_argument(
|
||||
'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
|
||||
@cmd2.with_argparser(unblock_chv_parser)
|
||||
def do_unblock_chv(self, opts):
|
||||
"""Unblock PIN code using specified PUK code"""
|
||||
new_pin = self.get_code(opts.NEWPIN, "PIN" + str(opts.pin_nr))
|
||||
puk = self.get_code(opts.PUK, "PUK" + str(opts.pin_nr))
|
||||
new_pin = self.get_code(opts.new_pin_code)
|
||||
puk = self.get_code(opts.puk_code)
|
||||
(data, sw) = self._cmd.lchan.scc.unblock_chv(
|
||||
opts.pin_nr, h2b(puk), h2b(new_pin))
|
||||
self._cmd.poutput("CHV unblock successful")
|
||||
|
||||
change_chv_parser = argparse.ArgumentParser()
|
||||
change_chv_parser.add_argument('NEWPIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
change_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
change_chv_parser.add_argument(
|
||||
'--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
|
||||
change_chv_parser.add_argument(
|
||||
'pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
change_chv_parser.add_argument(
|
||||
'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
|
||||
@cmd2.with_argparser(change_chv_parser)
|
||||
def do_change_chv(self, opts):
|
||||
"""Change PIN code to a new PIN code"""
|
||||
new_pin = self.get_code(opts.NEWPIN, "PIN" + str(opts.pin_nr))
|
||||
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
|
||||
new_pin = self.get_code(opts.new_pin_code)
|
||||
pin = self.get_code(opts.pin_code)
|
||||
(data, sw) = self._cmd.lchan.scc.change_chv(
|
||||
opts.pin_nr, h2b(pin), h2b(new_pin))
|
||||
self._cmd.poutput("CHV change successful")
|
||||
@@ -967,26 +851,26 @@ class Iso7816Commands(CommandSet):
|
||||
disable_chv_parser = argparse.ArgumentParser()
|
||||
disable_chv_parser.add_argument(
|
||||
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
|
||||
disable_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
disable_chv_parser.add_argument(
|
||||
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
|
||||
@cmd2.with_argparser(disable_chv_parser)
|
||||
def do_disable_chv(self, opts):
|
||||
"""Disable PIN code using specified PIN code"""
|
||||
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
|
||||
pin = self.get_code(opts.pin_code)
|
||||
(data, sw) = self._cmd.lchan.scc.disable_chv(opts.pin_nr, h2b(pin))
|
||||
self._cmd.poutput("CHV disable successful")
|
||||
|
||||
enable_chv_parser = argparse.ArgumentParser()
|
||||
enable_chv_parser.add_argument(
|
||||
'--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
|
||||
enable_chv_parser.add_argument('PIN', nargs='?', type=is_decimal,
|
||||
help='PIN code value. If none given, CSV file will be queried')
|
||||
enable_chv_parser.add_argument(
|
||||
'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
|
||||
|
||||
@cmd2.with_argparser(enable_chv_parser)
|
||||
def do_enable_chv(self, opts):
|
||||
"""Enable PIN code using specified PIN code"""
|
||||
pin = self.get_code(opts.PIN, "PIN" + str(opts.pin_nr))
|
||||
pin = self.get_code(opts.pin_code)
|
||||
(data, sw) = self._cmd.lchan.scc.enable_chv(opts.pin_nr, h2b(pin))
|
||||
self._cmd.poutput("CHV enable successful")
|
||||
|
||||
@@ -998,15 +882,8 @@ class Iso7816Commands(CommandSet):
|
||||
activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
|
||||
@cmd2.with_argparser(activate_file_parser)
|
||||
def do_activate_file(self, opts):
|
||||
"""Activate the specified EF by sending an ACTIVATE FILE apdu command (used to be called REHABILITATE
|
||||
in TS 11.11 for classic SIM).
|
||||
|
||||
This command is used to (re-)activate a file that is currently in deactivated (sometimes also called
|
||||
"invalidated") state. You need to call this from the DF above the to-be-activated EF and specify the name or
|
||||
FID of the file to activate.
|
||||
|
||||
Note that for *deactivation* the to-be-deactivated EF must be selected, but for *activation*, the DF
|
||||
above the to-be-activated EF must be selected!"""
|
||||
"""Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
|
||||
SIM. You need to specify the name or FID of the file to activate."""
|
||||
(data, sw) = self._cmd.lchan.activate_file(opts.NAME)
|
||||
|
||||
def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
|
||||
@@ -1016,7 +893,7 @@ class Iso7816Commands(CommandSet):
|
||||
|
||||
open_chan_parser = argparse.ArgumentParser()
|
||||
open_chan_parser.add_argument(
|
||||
'chan_nr', type=int, default=1, choices=range(1,16), help='Channel Number')
|
||||
'chan_nr', type=int, default=0, help='Channel Number')
|
||||
|
||||
@cmd2.with_argparser(open_chan_parser)
|
||||
def do_open_channel(self, opts):
|
||||
@@ -1028,7 +905,7 @@ class Iso7816Commands(CommandSet):
|
||||
|
||||
close_chan_parser = argparse.ArgumentParser()
|
||||
close_chan_parser.add_argument(
|
||||
'chan_nr', type=int, default=1, choices=range(1,16), help='Channel Number')
|
||||
'chan_nr', type=int, default=0, help='Channel Number')
|
||||
|
||||
@cmd2.with_argparser(close_chan_parser)
|
||||
def do_close_channel(self, opts):
|
||||
@@ -1040,14 +917,14 @@ class Iso7816Commands(CommandSet):
|
||||
|
||||
switch_chan_parser = argparse.ArgumentParser()
|
||||
switch_chan_parser.add_argument(
|
||||
'chan_nr', type=int, default=0, choices=range(0,16), help='Channel Number')
|
||||
'chan_nr', type=int, default=0, help='Channel Number')
|
||||
|
||||
@cmd2.with_argparser(switch_chan_parser)
|
||||
def do_switch_channel(self, opts):
|
||||
"""Switch currently active logical channel."""
|
||||
self._cmd.lchan.unregister_cmds(self._cmd)
|
||||
self._cmd.lchan._select_pre(self._cmd)
|
||||
self._cmd.lchan = self._cmd.rs.lchan[opts.chan_nr]
|
||||
self._cmd.lchan.register_cmds(self._cmd)
|
||||
self._cmd.lchan._select_post(self._cmd)
|
||||
self._cmd.update_prompt()
|
||||
|
||||
def do_status(self, opts):
|
||||
@@ -1073,14 +950,8 @@ global_group.add_argument('--script', metavar='PATH', default=None,
|
||||
help='script with pySim-shell commands to be executed automatically at start-up')
|
||||
global_group.add_argument('--csv', metavar='FILE',
|
||||
default=None, help='Read card data from CSV file')
|
||||
global_group.add_argument('--csv-column-key', metavar='FIELD:AES_KEY_HEX', default=[], action='append',
|
||||
help='per-CSV-column AES transport key')
|
||||
global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
|
||||
help="Use automatic card handling machine")
|
||||
global_group.add_argument("--noprompt", help="Run in non interactive mode",
|
||||
action='store_true', default=False)
|
||||
global_group.add_argument("--skip-card-init", help="Skip all card/profile initialization",
|
||||
action='store_true', default=False)
|
||||
|
||||
adm_group = global_group.add_mutually_exclusive_group()
|
||||
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
|
||||
@@ -1088,8 +959,6 @@ adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', de
|
||||
adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
|
||||
help='ADM PIN used for provisioning, as hex string (16 characters long)')
|
||||
|
||||
option_parser.add_argument('-e', '--execute-command', action='append', default=[],
|
||||
help='A pySim-shell command that will be executed at startup')
|
||||
option_parser.add_argument("command", nargs='?',
|
||||
help="A pySim-shell command that would optionally be executed at startup")
|
||||
option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
|
||||
@@ -1098,20 +967,22 @@ option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
startup_errors = False
|
||||
# Parse options
|
||||
opts = option_parser.parse_args()
|
||||
|
||||
# If a script file is specified, be sure that it actually exists
|
||||
if opts.script:
|
||||
if not os.access(opts.script, os.R_OK):
|
||||
print("Invalid script file!")
|
||||
sys.exit(2)
|
||||
|
||||
# Register csv-file as card data provider, either from specified CSV
|
||||
# or from CSV file in home directory
|
||||
csv_column_keys = {}
|
||||
for par in opts.csv_column_key:
|
||||
name, key = par.split(':')
|
||||
csv_column_keys[name] = key
|
||||
csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
|
||||
if opts.csv:
|
||||
card_key_provider_register(CardKeyProviderCsv(opts.csv, csv_column_keys))
|
||||
card_key_provider_register(CardKeyProviderCsv(opts.csv))
|
||||
if os.path.isfile(csv_default):
|
||||
card_key_provider_register(CardKeyProviderCsv(csv_default, csv_column_keys))
|
||||
card_key_provider_register(CardKeyProviderCsv(csv_default))
|
||||
|
||||
# Init card reader driver
|
||||
sl = init_reader(opts, proactive_handler = Proact())
|
||||
@@ -1126,19 +997,20 @@ if __name__ == '__main__':
|
||||
# is no card in the reader or the card is unresponsive. PysimApp is
|
||||
# able to tolerate and recover from that.
|
||||
try:
|
||||
rs, card = init_card(sl, opts.skip_card_init)
|
||||
app = PysimApp(card, rs, sl, ch)
|
||||
rs, card = init_card(sl)
|
||||
app = PysimApp(card, rs, sl, ch, opts.script)
|
||||
except:
|
||||
startup_errors = True
|
||||
print("Card initialization (%s) failed with an exception:" % str(sl))
|
||||
print("---------------------8<---------------------")
|
||||
traceback.print_exc()
|
||||
print("---------------------8<---------------------")
|
||||
if not opts.noprompt:
|
||||
print("(you may still try to recover from this manually by using the 'equip' command.)")
|
||||
print(" it should also be noted that some readers may behave strangely when no card")
|
||||
print(" is inserted.)")
|
||||
print("")
|
||||
print("(you may still try to recover from this manually by using the 'equip' command.)")
|
||||
print(
|
||||
" it should also be noted that some readers may behave strangely when no card")
|
||||
print(" is inserted.)")
|
||||
print("")
|
||||
if opts.script:
|
||||
print("will not execute startup script due to card initialization errors!")
|
||||
app = PysimApp(None, None, sl, ch)
|
||||
|
||||
# If the user supplies an ADM PIN at via commandline args authenticate
|
||||
@@ -1150,44 +1022,9 @@ if __name__ == '__main__':
|
||||
try:
|
||||
card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
|
||||
except Exception as e:
|
||||
startup_errors = True
|
||||
print("ADM verification (%s) failed with an exception:" % str(pin_adm))
|
||||
print("---------------------8<---------------------")
|
||||
print(e)
|
||||
print("---------------------8<---------------------")
|
||||
|
||||
# Run optional commands
|
||||
for c in opts.execute_command:
|
||||
if not startup_errors:
|
||||
stop = app.onecmd_plus_hooks(c)
|
||||
if stop == True:
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("Errors during startup, refusing to execute command (%s)" % c)
|
||||
|
||||
# Run optional command
|
||||
if opts.command:
|
||||
if not startup_errors:
|
||||
stop = app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
|
||||
if stop == True:
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("Errors during startup, refusing to execute command (%s)" % opts.command)
|
||||
|
||||
# Run optional script file
|
||||
if opts.script:
|
||||
if not startup_errors:
|
||||
if not os.access(opts.script, os.R_OK):
|
||||
print("Error: script file (%s) not readable!" % opts.script)
|
||||
startup_errors = True
|
||||
else:
|
||||
stop = app.onecmd_plus_hooks('{} {}'.format('run_script', opts.script), add_to_history = False)
|
||||
if stop == True:
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("Errors during startup, refusing to execute script (%s)" % opts.script)
|
||||
|
||||
if not opts.noprompt:
|
||||
app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
|
||||
else:
|
||||
app.cmdloop()
|
||||
elif startup_errors:
|
||||
sys.exit(2)
|
||||
|
||||
@@ -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 proective 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,22 +8,17 @@ from pprint import pprint as pp
|
||||
from pySim.apdu import *
|
||||
from pySim.runtime import RuntimeState
|
||||
|
||||
from osmocom.utils import JsonEncoder
|
||||
|
||||
from pySim.cards import UiccCardBase
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.ts_31_102 import CardApplicationUSIM
|
||||
from pySim.ts_31_103 import CardApplicationISIM
|
||||
from pySim.euicc import CardApplicationISDR, CardApplicationECASD
|
||||
from pySim.transport import LinkBase
|
||||
|
||||
from pySim.apdu_source.gsmtap import GsmtapApduSource
|
||||
from pySim.apdu_source.pyshark_rspro import PysharkRsproPcap, PysharkRsproLive
|
||||
from pySim.apdu_source.pyshark_gsmtap import PysharkGsmtapPcap
|
||||
from pySim.apdu_source.tca_loader_log import TcaLoaderLogApduSource
|
||||
from pySim.apdu_source.stdin_hex import StdinHexApduSource
|
||||
|
||||
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
|
||||
|
||||
@@ -33,11 +28,10 @@ logger = colorlog.getLogger()
|
||||
|
||||
# merge all of the command sets into one global set. This will override instructions,
|
||||
# the one from the 'last' set in the addition below will prevail.
|
||||
from pySim.apdu.ts_51_011 import ApduCommands as SimApduCommands
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
ApduCommands = SimApduCommands + UiccApduCommands + UsimApduCommands #+ GpApduCommands
|
||||
ApduCommands = UiccApduCommands + UsimApduCommands #+ GpApduCommands
|
||||
|
||||
|
||||
class DummySimLink(LinkBase):
|
||||
@@ -57,7 +51,7 @@ class DummySimLink(LinkBase):
|
||||
def __str__(self):
|
||||
return "dummy"
|
||||
|
||||
def _send_apdu(self, pdu):
|
||||
def _send_apdu_raw(self, pdu):
|
||||
#print("DummySimLink-apdu: %s" % pdu)
|
||||
return [], '9000'
|
||||
|
||||
@@ -67,7 +61,7 @@ class DummySimLink(LinkBase):
|
||||
def disconnect(self):
|
||||
pass
|
||||
|
||||
def _reset_card(self):
|
||||
def reset_card(self):
|
||||
return 1
|
||||
|
||||
def get_atr(self):
|
||||
@@ -84,8 +78,6 @@ class Tracer:
|
||||
profile = CardProfileUICC()
|
||||
profile.add_application(CardApplicationUSIM())
|
||||
profile.add_application(CardApplicationISIM())
|
||||
profile.add_application(CardApplicationISDR())
|
||||
profile.add_application(CardApplicationECASD())
|
||||
scc = SimCardCommands(transport=DummySimLink())
|
||||
card = UiccCardBase(scc)
|
||||
self.rs = RuntimeState(card, profile)
|
||||
@@ -101,8 +93,7 @@ class Tracer:
|
||||
"""Output a single decoded + processed ApduCommand."""
|
||||
if self.show_raw_apdu:
|
||||
print(apdu)
|
||||
print("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id,
|
||||
inst.col_sw, json.dumps(inst.processed, cls=JsonEncoder)))
|
||||
print("%02u %-16s %-35s %-8s %s %s" % (inst.lchan_nr, inst._name, inst.path_str, inst.col_id, inst.col_sw, inst.processed))
|
||||
print("===============================")
|
||||
|
||||
def format_reset(self, apdu: CardReset):
|
||||
@@ -187,15 +178,6 @@ parser_rspro_pyshark_live = subparsers.add_parser('rspro-pyshark-live', help="""
|
||||
parser_rspro_pyshark_live.add_argument('-i', '--interface', required=True,
|
||||
help='Name of the network interface to capture on')
|
||||
|
||||
parser_tcaloader_log = subparsers.add_parser('tca-loader-log', help="""
|
||||
Read APDUs from a TCA Loader log file.""")
|
||||
parser_tcaloader_log.add_argument('-f', '--log-file', required=True,
|
||||
help='Name of te log file to be read')
|
||||
|
||||
parser_stdin_hex = subparsers.add_parser('stdin-hex', help="""
|
||||
Read APDUs as hex-string from stdin.""")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
opts = option_parser.parse_args()
|
||||
@@ -209,12 +191,6 @@ if __name__ == '__main__':
|
||||
s = PysharkRsproLive(opts.interface)
|
||||
elif opts.source == 'gsmtap-pyshark-pcap':
|
||||
s = PysharkGsmtapPcap(opts.pcap_file)
|
||||
elif opts.source == 'tca-loader-log':
|
||||
s = TcaLoaderLogApduSource(opts.log_file)
|
||||
elif opts.source == 'stdin-hex':
|
||||
s = StdinHexApduSource()
|
||||
else:
|
||||
raise ValueError("unsupported source %s", opts.source)
|
||||
|
||||
tracer = Tracer(source=s, suppress_status=opts.suppress_status, suppress_select=opts.suppress_select,
|
||||
show_raw_apdu=opts.show_raw_apdu)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# coding=utf-8
|
||||
"""APDU (and TPDU) parser for UICC/USIM/ISIM cards.
|
||||
|
||||
The File (and its classes) represent the structure / hierarchy
|
||||
@@ -9,7 +10,7 @@ is far too simplistic, while this decoder can utilize all of the information
|
||||
we already know in pySim about the filesystem structure, file encoding, etc.
|
||||
"""
|
||||
|
||||
# (C) 2022-2024 by Harald Welte <laforge@osmocom.org>
|
||||
# (C) 2022 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -26,14 +27,14 @@ we already know in pySim about the filesystem structure, file encoding, etc.
|
||||
|
||||
|
||||
import abc
|
||||
from termcolor import colored
|
||||
import typing
|
||||
from typing import List, Dict, Optional
|
||||
from termcolor import colored
|
||||
from construct import Byte
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
|
||||
from construct import *
|
||||
from construct import Optional as COptional
|
||||
from pySim.construct import *
|
||||
from pySim.utils import *
|
||||
from pySim.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
|
||||
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
|
||||
|
||||
@@ -51,8 +52,8 @@ from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF
|
||||
class ApduCommandMeta(abc.ABCMeta):
|
||||
"""A meta-class that we can use to set some class variables when declaring
|
||||
a derived class of ApduCommand."""
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
x = super().__new__(mcs, name, bases, namespace)
|
||||
def __new__(metacls, name, bases, namespace, **kwargs):
|
||||
x = super().__new__(metacls, name, bases, namespace)
|
||||
x._name = namespace.get('name', kwargs.get('n', None))
|
||||
x._ins = namespace.get('ins', kwargs.get('ins', None))
|
||||
x._cla = namespace.get('cla', kwargs.get('cla', None))
|
||||
@@ -149,10 +150,8 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
# fall-back constructs if the derived class provides no override
|
||||
_construct_p1 = Byte
|
||||
_construct_p2 = Byte
|
||||
_construct = GreedyBytes
|
||||
_construct_rsp = GreedyBytes
|
||||
_tlv = None
|
||||
_tlv_rsp = None
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
_construct_rsp = HexAdapter(GreedyBytes)
|
||||
|
||||
def __init__(self, cmd: BytesOrHex, rsp: Optional[BytesOrHex] = None):
|
||||
"""Instantiate a new ApduCommand from give cmd + resp."""
|
||||
@@ -188,39 +187,44 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
if apdu_case in [1, 2]:
|
||||
# data is part of response
|
||||
return cls(buffer[:5], buffer[5:])
|
||||
if apdu_case in [3, 4]:
|
||||
elif apdu_case in [3, 4]:
|
||||
# data is part of command
|
||||
lc = buffer[4]
|
||||
return cls(buffer[:5+lc], buffer[5+lc:])
|
||||
raise ValueError('%s: Invalid APDU Case %u' % (cls.__name__, apdu_case))
|
||||
else:
|
||||
raise ValueError('%s: Invalid APDU Case %u' % (cls.__name__, apdu_case))
|
||||
|
||||
@property
|
||||
def path(self) -> List[str]:
|
||||
"""Return (if known) the path as list of files to the file on which this command operates."""
|
||||
if self.file:
|
||||
return self.file.fully_qualified_path()
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
|
||||
@property
|
||||
def path_str(self) -> str:
|
||||
"""Return (if known) the path as string to the file on which this command operates."""
|
||||
if self.file:
|
||||
return self.file.fully_qualified_path_str()
|
||||
return ''
|
||||
else:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def col_sw(self) -> str:
|
||||
"""Return the ansi-colorized status word. Green==OK, Red==Error"""
|
||||
if self.successful:
|
||||
return colored(b2h(self.sw), 'green')
|
||||
return colored(b2h(self.sw), 'red')
|
||||
else:
|
||||
return colored(b2h(self.sw), 'red')
|
||||
|
||||
@property
|
||||
def lchan_nr(self) -> int:
|
||||
"""Logical channel number over which this ApduCommand was transmitted."""
|
||||
if self.lchan:
|
||||
return self.lchan.lchan_nr
|
||||
return lchan_nr_from_cla(self.cla)
|
||||
else:
|
||||
return lchan_nr_from_cla(self.cla)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '%02u %s(%s): %s' % (self.lchan_nr, type(self).__name__, self.path_str, self.to_dict())
|
||||
@@ -232,7 +236,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
"""Fall-back function to be called if there is no derived-class-specific
|
||||
process_global or process_on_lchan method. Uses information from APDU decode."""
|
||||
self.processed = {}
|
||||
if 'p1' not in self.cmd_dict:
|
||||
if not 'p1' in self.cmd_dict:
|
||||
self.processed = self.to_dict()
|
||||
else:
|
||||
self.processed['p1'] = self.cmd_dict['p1']
|
||||
@@ -271,7 +275,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
"""Does the given CLA match the CLA list of the command?."""
|
||||
if not isinstance(cla, str):
|
||||
cla = '%02X' % cla
|
||||
cla = cla.upper()
|
||||
cla = cla.lower()
|
||||
# see https://github.com/PyCQA/pylint/issues/7219
|
||||
# pylint: disable=no-member
|
||||
for cla_match in cls._cla:
|
||||
@@ -281,7 +285,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
cla_masked += 'X'
|
||||
else:
|
||||
cla_masked += cla[i]
|
||||
if cla_masked == cla_match.upper():
|
||||
if cla_masked == cla_match:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -291,26 +295,17 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
if callable(method):
|
||||
return method()
|
||||
else:
|
||||
return self._cmd_to_dict()
|
||||
|
||||
def _cmd_to_dict(self) -> Dict:
|
||||
"""back-end function performing automatic decoding using _construct / _tlv."""
|
||||
r = {}
|
||||
method = getattr(self, '_decode_p1p2', None)
|
||||
if callable(method):
|
||||
r = self._decode_p1p2()
|
||||
else:
|
||||
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
|
||||
r['p2'] = parse_construct(self._construct_p2, self.p2.to_bytes(1, 'big'))
|
||||
r['p3'] = self.p3
|
||||
if self.cmd_data:
|
||||
if self._tlv:
|
||||
ie = self._tlv()
|
||||
ie.from_tlv(self.cmd_data)
|
||||
r['body'] = ie.to_dict()
|
||||
r = {}
|
||||
method = getattr(self, '_decode_p1p2', None)
|
||||
if callable(method):
|
||||
r = self._decode_p1p2()
|
||||
else:
|
||||
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
|
||||
r['p2'] = parse_construct(self._construct_p2, self.p2.to_bytes(1, 'big'))
|
||||
r['p3'] = self.p3
|
||||
if self.cmd_data:
|
||||
r['body'] = parse_construct(self._construct, self.cmd_data)
|
||||
return r
|
||||
return r
|
||||
|
||||
def rsp_to_dict(self) -> Dict:
|
||||
"""Convert the Response part of the APDU to a dict."""
|
||||
@@ -320,12 +315,7 @@ class ApduCommand(Apdu, metaclass=ApduCommandMeta):
|
||||
else:
|
||||
r = {}
|
||||
if self.rsp_data:
|
||||
if self._tlv_rsp:
|
||||
ie = self._tlv_rsp()
|
||||
ie.from_tlv(self.rsp_data)
|
||||
r['body'] = ie.to_dict()
|
||||
else:
|
||||
r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
|
||||
r['body'] = parse_construct(self._construct_rsp, self.rsp_data)
|
||||
r['sw'] = b2h(self.sw)
|
||||
return r
|
||||
|
||||
@@ -438,7 +428,8 @@ class TpduFilter(ApduHandler):
|
||||
apdu = Apdu(icmd, tpdu.rsp)
|
||||
if self.apdu_handler:
|
||||
return self.apdu_handler.input(apdu)
|
||||
return Apdu(icmd, tpdu.rsp)
|
||||
else:
|
||||
return Apdu(icmd, tpdu.rsp)
|
||||
|
||||
def input(self, cmd: bytes, rsp: bytes):
|
||||
if isinstance(cmd, str):
|
||||
@@ -461,6 +452,7 @@ class CardReset:
|
||||
self.atr = atr
|
||||
|
||||
def __str__(self):
|
||||
if self.atr:
|
||||
if (self.atr):
|
||||
return '%s(%s)' % (type(self).__name__, b2h(self.atr))
|
||||
return '%s' % (type(self).__name__)
|
||||
else:
|
||||
return '%s' % (type(self).__name__)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# coding=utf-8
|
||||
"""APDU definition/decoder of GlobalPLatform Card Spec (currently 2.1.1)
|
||||
|
||||
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
|
||||
(C) 2022 by Harald Welte <laforge@osmocom.org>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -17,11 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from construct import FlagsEnum, Struct
|
||||
from osmocom.tlv import flatten_dict_lists
|
||||
from osmocom.construct import *
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
from pySim.global_platform import InstallParameters
|
||||
|
||||
class GpDelete(ApduCommand, n='DELETE', ins=0xE4, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 4
|
||||
@@ -44,29 +40,8 @@ class GpGetDataCB(ApduCommand, n='GET DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
|
||||
class GpGetStatus(ApduCommand, n='GET STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 4
|
||||
|
||||
# GPCS Section 11.5.2
|
||||
class GpInstall(ApduCommand, n='INSTALL', ins=0xE6, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 4
|
||||
_construct_p1 = FlagsEnum(Byte, more_commands=0x80, for_registry_update=0x40,
|
||||
for_personalization=0x20, for_extradition=0x10,
|
||||
for_make_selectable=0x08, for_install=0x04, for_load=0x02)
|
||||
_construct_p2 = Enum(Byte, no_info_provided=0x00, beginning_of_combined=0x01,
|
||||
end_of_combined=0x03)
|
||||
_construct = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'module_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'application_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'privileges'/Prefixed(Int8ub, GreedyBytes),
|
||||
'install_parameters'/Prefixed(Int8ub, GreedyBytes), # TODO: InstallParameters
|
||||
'install_token'/Prefixed(Int8ub, GreedyBytes))
|
||||
def _decode_cmd(self):
|
||||
# first use _construct* above
|
||||
res = self._cmd_to_dict()
|
||||
# then do TLV decode of install_parameters
|
||||
ip = InstallParameters()
|
||||
ip.from_tlv(res['body']['install_parameters'])
|
||||
res['body']['install_parameters'] = flatten_dict_lists(ip.to_dict())
|
||||
return res
|
||||
|
||||
|
||||
class GpLoad(ApduCommand, n='LOAD', ins=0xE8, cla=['8X', 'CX', 'EX']):
|
||||
_apdu_case = 4
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# coding=utf-8
|
||||
"""APDU definitions/decoders of ETSI TS 102 221, the core UICC spec.
|
||||
|
||||
(C) 2022-2024 by Harald Welte <laforge@osmocom.org>
|
||||
(C) 2022 by Harald Welte <laforge@osmocom.org>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -17,18 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict
|
||||
import logging
|
||||
|
||||
from construct import GreedyRange, Struct
|
||||
|
||||
from osmocom.utils import i2h
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.construct import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.runtime import RuntimeLchan
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
from pySim import cat
|
||||
from typing import Optional, Dict, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -100,7 +94,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
|
||||
logger.warning('SELECT UNKNOWN FID %s', file_hex)
|
||||
elif mode == 'df_name':
|
||||
# Select by AID (can be sub-string!)
|
||||
aid = b2h(self.cmd_dict['body'])
|
||||
aid = self.cmd_dict['body']
|
||||
sels = lchan.rs.mf.get_app_selectables(['AIDS'])
|
||||
adf = self._find_aid_substr(sels, aid)
|
||||
if adf:
|
||||
@@ -109,6 +103,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
|
||||
#print("\tSELECT AID %s" % adf)
|
||||
else:
|
||||
logger.warning('SELECT UNKNOWN AID %s', aid)
|
||||
pass
|
||||
else:
|
||||
raise ValueError('Select Mode %s not implemented' % mode)
|
||||
# decode the SELECT response
|
||||
@@ -116,7 +111,7 @@ class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
|
||||
self.file = lchan.selected_file
|
||||
if 'body' in self.rsp_dict:
|
||||
# not every SELECT is asking for the FCP in response...
|
||||
return lchan.selected_file.decode_select_response(b2h(self.rsp_dict['body']))
|
||||
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
|
||||
return None
|
||||
|
||||
|
||||
@@ -129,7 +124,7 @@ class UiccStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
|
||||
|
||||
def process_on_lchan(self, lchan):
|
||||
if self.cmd_dict['p2'] == 'response_like_select':
|
||||
return lchan.selected_file.decode_select_response(b2h(self.rsp_dict['body']))
|
||||
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
|
||||
|
||||
def _decode_binary_p1p2(p1, p2) -> Dict:
|
||||
ret = {}
|
||||
@@ -295,9 +290,12 @@ class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
|
||||
|
||||
@staticmethod
|
||||
def _pin_is_success(sw):
|
||||
return bool(sw[0] == 0x63)
|
||||
if sw[0] == 0x63:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -309,7 +307,7 @@ class ChangePin(ApduCommand, n='CHANGE PIN', ins=0x24, cla=['0X', '4X', '6X']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -321,7 +319,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -332,7 +330,7 @@ class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X'])
|
||||
class EnablePin(ApduCommand, n='ENABLE PIN', ins=0x28, cla=['0X', '4X', '6X']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -344,7 +342,7 @@ class UnblockPin(ApduCommand, n='UNBLOCK PIN', ins=0x2C, cla=['0X', '4X', '6X'])
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, _lchan: RuntimeLchan):
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyPin._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
@@ -397,12 +395,13 @@ class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X',
|
||||
manage_channel.add_lchan(created_channel_nr)
|
||||
self.col_id = '%02u' % created_channel_nr
|
||||
return {'mode': mode, 'created_channel': created_channel_nr }
|
||||
if mode == 'close_channel':
|
||||
elif mode == 'close_channel':
|
||||
closed_channel_nr = self.cmd_dict['p2']['logical_channel_number']
|
||||
rs.del_lchan(closed_channel_nr)
|
||||
self.col_id = '%02u' % closed_channel_nr
|
||||
return {'mode': mode, 'closed_channel': closed_channel_nr }
|
||||
raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
|
||||
else:
|
||||
raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
|
||||
|
||||
# TS 102 221 Section 11.1.18
|
||||
class GetChallenge(ApduCommand, n='GET CHALLENGE', ins=0x84, cla=['0X', '4X', '6X']):
|
||||
@@ -420,13 +419,13 @@ class ManageSecureChannel(ApduCommand, n='MANAGE SECURE CHANNEL', ins=0x73, cla=
|
||||
p2 = hdr[3]
|
||||
if p1 & 0x7 == 0: # retrieve UICC Endpoints
|
||||
return 2
|
||||
if p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
|
||||
elif p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
|
||||
p2_cmd = p2 >> 5
|
||||
if p2_cmd in [0,2,4]: # command data
|
||||
return 3
|
||||
if p2_cmd in [1,3,5]: # response data
|
||||
elif p2_cmd in [1,3,5]: # response data
|
||||
return 2
|
||||
if p1 & 0xf == 4: # terminate secure channel SA
|
||||
elif p1 & 0xf == 4: # terminate secure channel SA
|
||||
return 3
|
||||
raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
|
||||
|
||||
@@ -437,7 +436,8 @@ class TransactData(ApduCommand, n='TRANSACT DATA', ins=0x75, cla=['0X', '4X', '6
|
||||
p1 = hdr[2]
|
||||
if p1 & 0x04:
|
||||
return 3
|
||||
return 2
|
||||
else:
|
||||
return 2
|
||||
|
||||
# TS 102 221 Section 11.1.22
|
||||
class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
|
||||
@@ -460,17 +460,14 @@ class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['80']):
|
||||
# TS 102 221 Section 11.2.2 / TS 102 223
|
||||
class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
|
||||
_apdu_case = 4
|
||||
_tlv = cat.EventCollection
|
||||
|
||||
# TS 102 221 Section 11.2.3 / TS 102 223
|
||||
class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
|
||||
_apdu_case = 2
|
||||
_tlv_rsp = cat.ProactiveCommand
|
||||
|
||||
# TS 102 221 Section 11.2.3 / TS 102 223
|
||||
class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
|
||||
_apdu_case = 3
|
||||
_tlv = cat.TerminalResponse
|
||||
|
||||
# TS 102 221 Section 11.3.1
|
||||
class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
|
||||
|
||||
@@ -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 construct import BitStruct, Enum, BitsInteger, Int8ub, this, Struct, If, Switch, Const
|
||||
from construct import *
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.filesystem import *
|
||||
from pySim.construct import *
|
||||
from pySim.ts_31_102 import SUCI_TlvDataObject
|
||||
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
|
||||
# Copyright (C) 2022 Harald Welte <laforge@osmocom.org>
|
||||
@@ -35,6 +35,8 @@ from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
|
||||
# Mapping between USIM Service Number and its description
|
||||
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
|
||||
# TS 31.102 Section 7.1
|
||||
class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '4X', '6X']):
|
||||
_apdu_case = 4
|
||||
@@ -42,28 +44,28 @@ class UsimAuthenticateEven(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '
|
||||
BitsInteger(4),
|
||||
'authentication_context'/Enum(BitsInteger(3), gsm=0, umts=1,
|
||||
vgcs_vbs=2, gba=4))
|
||||
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
|
||||
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, Bytes(this._autn_len)))
|
||||
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/Bytes(this._vsid_len),
|
||||
'_vkid_len'/Int8ub, 'vk_id'/Bytes(this._vkid_len),
|
||||
'_vstk_rand_len'/Int8ub, 'vstk_rand'/Bytes(this._vstk_rand_len))
|
||||
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/Bytes(this._rand_len),
|
||||
'_autn_len'/Int8ub, 'autn'/Bytes(this._autn_len))
|
||||
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/Bytes(this._naf_id_len),
|
||||
'_impi_len'/Int8ub, 'impi'/Bytes(this._impi_len))
|
||||
_cs_cmd_gsm_3g = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
|
||||
'_autn_len'/COptional(Int8ub), 'autn'/If(this._autn_len, HexAdapter(Bytes(this._autn_len))))
|
||||
_cs_cmd_vgcs = Struct('_vsid_len'/Int8ub, 'vservice_id'/HexAdapter(Bytes(this._vsid_len)),
|
||||
'_vkid_len'/Int8ub, 'vk_id'/HexAdapter(Bytes(this._vkid_len)),
|
||||
'_vstk_rand_len'/Int8ub, 'vstk_rand'/HexAdapter(Bytes(this._vstk_rand_len)))
|
||||
_cmd_gba_bs = Struct('_rand_len'/Int8ub, 'rand'/HexAdapter(Bytes(this._rand_len)),
|
||||
'_autn_len'/Int8ub, 'autn'/HexAdapter(Bytes(this._autn_len)))
|
||||
_cmd_gba_naf = Struct('_naf_id_len'/Int8ub, 'naf_id'/HexAdapter(Bytes(this._naf_id_len)),
|
||||
'_impi_len'/Int8ub, 'impi'/HexAdapter(Bytes(this._impi_len)))
|
||||
_cs_cmd_gba = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDD: 'bootstrap'/_cmd_gba_bs,
|
||||
0xDE: 'naf_derivation'/_cmd_gba_naf }))
|
||||
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/Bytes(this._len_sres),
|
||||
'_len_kc'/Int8ub, 'kc'/Bytes(this._len_kc))
|
||||
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/Bytes(this._len_res),
|
||||
'_len_ck'/Int8ub, 'ck'/Bytes(this._len_ck),
|
||||
'_len_ik'/Int8ub, 'ik'/Bytes(this._len_ik),
|
||||
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, Bytes(this._len_kc)))
|
||||
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/Bytes(this._len_auts))
|
||||
_cs_rsp_gsm = Struct('_len_sres'/Int8ub, 'sres'/HexAdapter(Bytes(this._len_sres)),
|
||||
'_len_kc'/Int8ub, 'kc'/HexAdapter(Bytes(this._len_kc)))
|
||||
_rsp_3g_ok = Struct('_len_res'/Int8ub, 'res'/HexAdapter(Bytes(this._len_res)),
|
||||
'_len_ck'/Int8ub, 'ck'/HexAdapter(Bytes(this._len_ck)),
|
||||
'_len_ik'/Int8ub, 'ik'/HexAdapter(Bytes(this._len_ik)),
|
||||
'_len_kc'/COptional(Int8ub), 'kc'/If(this._len_kc, HexAdapter(Bytes(this._len_kc))))
|
||||
_rsp_3g_sync = Struct('_len_auts'/Int8ub, 'auts'/HexAdapter(Bytes(this._len_auts)))
|
||||
_cs_rsp_3g = Struct('tag'/Int8ub, 'body'/Switch(this.tag, { 0xDB: 'success'/_rsp_3g_ok,
|
||||
0xDC: 'sync_fail'/_rsp_3g_sync}))
|
||||
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/Bytes(this._vstk_len))
|
||||
_cs_rsp_gba_naf = Struct(Const(b'\xDB'), '_ks_ext_naf_len'/Int8ub, 'ks_ext_naf'/Bytes(this._ks_ext_naf_len))
|
||||
_cs_rsp_vgcs = Struct(Const(b'\xDB'), '_vstk_len'/Int8ub, 'vstk'/HexAdapter(Bytes(this._vstk_len)))
|
||||
_cs_rsp_gba_naf = Struct(Const(b'\xDB'), '_ks_ext_naf_len'/Int8ub, 'ks_ext_naf'/HexAdapter(Bytes(this._ks_ext_naf_len)))
|
||||
def _decode_cmd(self) -> Dict:
|
||||
r = {}
|
||||
r['p1'] = parse_construct(self._construct_p1, self.p1.to_bytes(1, 'big'))
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
# coding=utf-8
|
||||
"""APDU definitions/decoders of 3GPP TS 51.011, the classic SIM spec.
|
||||
|
||||
(C) 2022 by Harald Welte <laforge@osmocom.org>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from construct import GreedyRange, Struct
|
||||
from pySim.construct import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.runtime import RuntimeLchan
|
||||
from pySim.apdu import ApduCommand, ApduCommandSet
|
||||
from typing import Optional, Dict, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TS 51.011 Section 9.2.1
|
||||
class SimSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['A0']):
|
||||
_apdu_case = 4
|
||||
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
path = [self.cmd_data[i:i+2] for i in range(0, len(self.cmd_data), 2)]
|
||||
for file in path:
|
||||
file_hex = b2h(file)
|
||||
sels = lchan.selected_file.get_selectables(['FIDS'])
|
||||
if file_hex in sels:
|
||||
if self.successful:
|
||||
#print("\tSELECT %s" % sels[file_hex])
|
||||
lchan.selected_file = sels[file_hex]
|
||||
else:
|
||||
#print("\tSELECT %s FAILED" % sels[file_hex])
|
||||
pass
|
||||
continue
|
||||
logger.warning('SELECT UNKNOWN FID %s (%s)' % (file_hex, '/'.join([b2h(x) for x in path])))
|
||||
if len(self.cmd_data) != 2:
|
||||
raise ValueError('Expecting a 2-byte FID')
|
||||
|
||||
# decode the SELECT response
|
||||
if self.successful:
|
||||
self.file = lchan.selected_file
|
||||
if 'body' in self.rsp_dict:
|
||||
# not every SELECT is asking for the FCP in response...
|
||||
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
|
||||
return None
|
||||
|
||||
|
||||
# TS 51.011 Section 9.2.2
|
||||
class SimStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['A0']):
|
||||
_apdu_case = 2
|
||||
|
||||
def process_on_lchan(self, lchan):
|
||||
if self.successful:
|
||||
if 'body' in self.rsp_dict:
|
||||
return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
|
||||
|
||||
def _decode_binary_p1p2(p1, p2) -> Dict:
|
||||
ret = {}
|
||||
if p1 & 0x80:
|
||||
ret['file'] = 'sfi'
|
||||
ret['sfi'] = p1 & 0x1f
|
||||
ret['offset'] = p2
|
||||
else:
|
||||
ret['file'] = 'currently_selected_ef'
|
||||
ret['offset'] = ((p1 & 0x7f) << 8) & p2
|
||||
return ret
|
||||
|
||||
# TS 51.011 Section 9.2.3 / 31.101
|
||||
class ReadBinary(ApduCommand, n='READ BINARY', ins=0xB0, cla=['A0']):
|
||||
_apdu_case = 2
|
||||
def _decode_p1p2(self):
|
||||
return _decode_binary_p1p2(self.p1, self.p2)
|
||||
|
||||
def process_on_lchan(self, lchan):
|
||||
self._determine_file(lchan)
|
||||
if not isinstance(self.file, TransparentEF):
|
||||
return b2h(self.rsp_data)
|
||||
# our decoders don't work for non-zero offsets / short reads
|
||||
if self.cmd_dict['offset'] != 0 or self.lr < self.file.size[0]:
|
||||
return b2h(self.rsp_data)
|
||||
method = getattr(self.file, 'decode_bin', None)
|
||||
if self.successful and callable(method):
|
||||
return method(self.rsp_data)
|
||||
|
||||
# TS 51.011 Section 9.2.4 / 31.101
|
||||
class UpdateBinary(ApduCommand, n='UPDATE BINARY', ins=0xD6, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
def _decode_p1p2(self):
|
||||
return _decode_binary_p1p2(self.p1, self.p2)
|
||||
|
||||
def process_on_lchan(self, lchan):
|
||||
self._determine_file(lchan)
|
||||
if not isinstance(self.file, TransparentEF):
|
||||
return b2h(self.rsp_data)
|
||||
# our decoders don't work for non-zero offsets / short writes
|
||||
if self.cmd_dict['offset'] != 0 or self.lc < self.file.size[0]:
|
||||
return b2h(self.cmd_data)
|
||||
method = getattr(self.file, 'decode_bin', None)
|
||||
if self.successful and callable(method):
|
||||
return method(self.cmd_data)
|
||||
|
||||
def _decode_record_p1p2(p1, p2):
|
||||
ret = {}
|
||||
ret['record_number'] = p1
|
||||
if p2 >> 3 == 0:
|
||||
ret['file'] = 'currently_selected_ef'
|
||||
else:
|
||||
ret['file'] = 'sfi'
|
||||
ret['sfi'] = p2 >> 3
|
||||
mode = p2 & 0x7
|
||||
if mode == 2:
|
||||
ret['mode'] = 'next_record'
|
||||
elif mode == 3:
|
||||
ret['mode'] = 'previous_record'
|
||||
elif mode == 8:
|
||||
ret['mode'] = 'absolute_current'
|
||||
return ret
|
||||
|
||||
# TS 51.011 Section 9.2.5
|
||||
class ReadRecord(ApduCommand, n='READ RECORD', ins=0xB2, cla=['A0']):
|
||||
_apdu_case = 2
|
||||
def _decode_p1p2(self):
|
||||
r = _decode_record_p1p2(self.p1, self.p2)
|
||||
self.col_id = '%02u' % r['record_number']
|
||||
return r
|
||||
|
||||
def process_on_lchan(self, lchan):
|
||||
self._determine_file(lchan)
|
||||
if not isinstance(self.file, LinFixedEF):
|
||||
return b2h(self.rsp_data)
|
||||
method = getattr(self.file, 'decode_record_bin', None)
|
||||
if self.successful and callable(method):
|
||||
return method(self.rsp_data)
|
||||
|
||||
# TS 51.011 Section 9.2.6
|
||||
class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
def _decode_p1p2(self):
|
||||
r = _decode_record_p1p2(self.p1, self.p2)
|
||||
self.col_id = '%02u' % r['record_number']
|
||||
return r
|
||||
|
||||
def process_on_lchan(self, lchan):
|
||||
self._determine_file(lchan)
|
||||
if not isinstance(self.file, LinFixedEF):
|
||||
return b2h(self.cmd_data)
|
||||
method = getattr(self.file, 'decode_record_bin', None)
|
||||
if self.successful and callable(method):
|
||||
return method(self.cmd_data)
|
||||
|
||||
# TS 51.011 Section 9.2.7
|
||||
class Seek(ApduCommand, n='SEEK', ins=0xA2, cla=['A0']):
|
||||
_apdu_case = 4
|
||||
_construct_rsp = GreedyRange(Int8ub)
|
||||
|
||||
def _decode_p1p2(self):
|
||||
ret = {}
|
||||
sfi = self.p2 >> 3
|
||||
if sfi == 0:
|
||||
ret['file'] = 'currently_selected_ef'
|
||||
else:
|
||||
ret['file'] = 'sfi'
|
||||
ret['sfi'] = sfi
|
||||
mode = self.p2 & 0x7
|
||||
if mode in [0x4, 0x5]:
|
||||
if mode == 0x4:
|
||||
ret['mode'] = 'forward_search'
|
||||
else:
|
||||
ret['mode'] = 'backward_search'
|
||||
ret['record_number'] = self.p1
|
||||
self.col_id = '%02u' % ret['record_number']
|
||||
elif mode == 6:
|
||||
ret['mode'] = 'enhanced_search'
|
||||
# TODO: further decode
|
||||
elif mode == 7:
|
||||
ret['mode'] = 'proprietary_search'
|
||||
return ret
|
||||
|
||||
def _decode_cmd(self):
|
||||
ret = self._decode_p1p2()
|
||||
if self.cmd_data:
|
||||
if ret['mode'] == 'enhanced_search':
|
||||
ret['search_indication'] = b2h(self.cmd_data[:2])
|
||||
ret['search_string'] = b2h(self.cmd_data[2:])
|
||||
else:
|
||||
ret['search_string'] = b2h(self.cmd_data)
|
||||
return ret
|
||||
|
||||
def process_on_lchan(self, lchan):
|
||||
self._determine_file(lchan)
|
||||
return self.to_dict()
|
||||
|
||||
# TS 51.011 Section 9.2.8
|
||||
class Increase(ApduCommand, n='INCREASE', ins=0x32, cla=['A0']):
|
||||
_apdu_case = 4
|
||||
|
||||
PinConstructP2 = BitStruct('scope'/Enum(Flag, global_mf=0, specific_df_adf=1),
|
||||
BitsInteger(2), 'reference_data_nr'/BitsInteger(5))
|
||||
|
||||
# TS 51.011 Section 9.2.9
|
||||
class VerifyChv(ApduCommand, n='VERIFY CHV', ins=0x20, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
@staticmethod
|
||||
def _pin_process(apdu):
|
||||
processed = {
|
||||
'scope': apdu.cmd_dict['p2']['scope'],
|
||||
'referenced_data_nr': apdu.cmd_dict['p2']['reference_data_nr'],
|
||||
}
|
||||
if apdu.lc == 0:
|
||||
# this is just a question on the counters remaining
|
||||
processed['mode'] = 'check_remaining_attempts'
|
||||
else:
|
||||
processed['pin'] = b2h(apdu.cmd_data)
|
||||
if apdu.sw[0] == 0x63:
|
||||
processed['remaining_attempts'] = apdu.sw[1] & 0xf
|
||||
return processed
|
||||
|
||||
@staticmethod
|
||||
def _pin_is_success(sw):
|
||||
if sw[0] == 0x63:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyChv._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
return VerifyChv._pin_is_success(self.sw)
|
||||
|
||||
|
||||
# TS 51.011 Section 9.2.10
|
||||
class ChangeChv(ApduCommand, n='CHANGE CHV', ins=0x24, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyChv._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
return VerifyChv._pin_is_success(self.sw)
|
||||
|
||||
|
||||
# TS 51.011 Section 9.2.11
|
||||
class DisableChv(ApduCommand, n='DISABLE CHV', ins=0x26, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyChv._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
return VerifyChv._pin_is_success(self.sw)
|
||||
|
||||
|
||||
# TS 51.011 Section 9.2.12
|
||||
class EnableChv(ApduCommand, n='ENABLE CHV', ins=0x28, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyChv._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
return VerifyChv._pin_is_success(self.sw)
|
||||
|
||||
|
||||
# TS 51.011 Section 9.2.13
|
||||
class UnblockChv(ApduCommand, n='UNBLOCK CHV', ins=0x2C, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
_construct_p2 = PinConstructP2
|
||||
|
||||
def process_on_lchan(self, lchan: RuntimeLchan):
|
||||
return VerifyChv._pin_process(self)
|
||||
|
||||
def _is_success(self):
|
||||
return VerifyChv._pin_is_success(self.sw)
|
||||
|
||||
|
||||
# TS 51.011 Section 9.2.14
|
||||
class Invalidate(ApduCommand, n='INVALIDATE', ins=0x04, cla=['A0']):
|
||||
_apdu_case = 1
|
||||
_construct_p1 = BitStruct(BitsInteger(4),
|
||||
'select_mode'/Enum(BitsInteger(4), ef_by_file_id=0,
|
||||
path_from_mf=8, path_from_current_df=9))
|
||||
|
||||
# TS 51.011 Section 9.2.15
|
||||
class Rehabilitate(ApduCommand, n='REHABILITATE', ins=0x44, cla=['A0']):
|
||||
_apdu_case = 1
|
||||
_construct_p1 = Invalidate._construct_p1
|
||||
|
||||
# TS 51.011 Section 9.2.16
|
||||
class RunGsmAlgorithm(ApduCommand, n='RUN GSM ALGORITHM', ins=0x88, cla=['A0']):
|
||||
_apdu_case = 4
|
||||
_construct = Struct('rand'/Bytes(16))
|
||||
_construct_rsp = Struct('sres'/Bytes(4), 'kc'/Bytes(8))
|
||||
|
||||
# TS 51.011 Section 9.2.17
|
||||
class Sleep(ApduCommand, n='SLEEP', ins=0xFA, cla=['A0']):
|
||||
_apdu_case = 2
|
||||
|
||||
# TS 51.011 Section 9.2.18
|
||||
class GetResponse(ApduCommand, n='GET RESPONSE', ins=0xC0, cla=['A0']):
|
||||
_apdu_case = 2
|
||||
|
||||
# TS 51.011 Section 9.2.19
|
||||
class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
|
||||
# TS 51.011 Section 9.2.20
|
||||
class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['A0']):
|
||||
_apdu_case = 4
|
||||
|
||||
# TS 51.011 Section 9.2.21
|
||||
class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['A0']):
|
||||
_apdu_case = 2
|
||||
|
||||
# TS 51.011 Section 9.2.22
|
||||
class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['A0']):
|
||||
_apdu_case = 3
|
||||
|
||||
|
||||
ApduCommands = ApduCommandSet('TS 51.011', cmds=[SimSelect, SimStatus, ReadBinary, UpdateBinary, ReadRecord,
|
||||
UpdateRecord, Seek, Increase, VerifyChv, ChangeChv, DisableChv,
|
||||
EnableChv, UnblockChv, Invalidate, Rehabilitate, RunGsmAlgorithm,
|
||||
Sleep, GetResponse, TerminalProfile, Envelope, Fetch, TerminalResponse])
|
||||
@@ -14,6 +14,7 @@ class ApduSource(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def read_packet(self) -> PacketType:
|
||||
"""Read one packet from the source."""
|
||||
pass
|
||||
|
||||
def read(self) -> Union[Apdu, CardReset]:
|
||||
"""Main function to call by the user: Blocking read, returns Apdu or CardReset."""
|
||||
@@ -30,5 +31,5 @@ class ApduSource(abc.ABC):
|
||||
elif isinstance(r, CardReset):
|
||||
apdu = r
|
||||
else:
|
||||
raise ValueError('Unknown read_packet() return %s' % r)
|
||||
ValueError('Unknown read_packet() return %s' % r)
|
||||
return apdu
|
||||
|
||||
@@ -16,16 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from osmocom.gsmtap import GsmtapReceiver
|
||||
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
|
||||
from pySim.gsmtap import GsmtapMessage, GsmtapSource
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
class GsmtapApduSource(ApduSource):
|
||||
"""ApduSource for handling GSMTAP-SIM messages received via UDP, such as
|
||||
@@ -41,19 +38,19 @@ class GsmtapApduSource(ApduSource):
|
||||
bind_port: UDP port number to which the socket should be bound (default: 4729)
|
||||
"""
|
||||
super().__init__()
|
||||
self.gsmtap = GsmtapReceiver(bind_ip, bind_port)
|
||||
self.gsmtap = GsmtapSource(bind_ip, bind_port)
|
||||
|
||||
def read_packet(self) -> PacketType:
|
||||
gsmtap_msg, _addr = self.gsmtap.read_packet()
|
||||
gsmtap_msg, addr = self.gsmtap.read_packet()
|
||||
if gsmtap_msg['type'] != 'sim':
|
||||
raise ValueError('Unsupported GSMTAP type %s' % gsmtap_msg['type'])
|
||||
sub_type = gsmtap_msg['sub_type']
|
||||
if sub_type == 'apdu':
|
||||
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
|
||||
if sub_type == 'atr':
|
||||
elif sub_type == 'atr':
|
||||
# card has been reset
|
||||
return CardReset(gsmtap_msg['body'])
|
||||
if sub_type in ['pps_req', 'pps_rsp']:
|
||||
elif sub_type in ['pps_req', 'pps_rsp']:
|
||||
# simply ignore for now
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -16,20 +16,21 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from pprint import pprint as pp
|
||||
from typing import Tuple
|
||||
import pyshark
|
||||
from osmocom.gsmtap import GsmtapMessage
|
||||
|
||||
from pySim.utils import h2b
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_102_222 import ApduCommands as UiccAdmApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
|
||||
from pySim.utils import h2b, b2h
|
||||
from pySim.apdu import Tpdu
|
||||
from pySim.gsmtap import GsmtapMessage
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
|
||||
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
|
||||
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
|
||||
from pySim.apdu.global_platform import ApduCommands as GpApduCommands
|
||||
ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -66,10 +67,10 @@ class _PysharkGsmtap(ApduSource):
|
||||
sub_type = gsmtap_msg['sub_type']
|
||||
if sub_type == 'apdu':
|
||||
return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
|
||||
if sub_type == 'atr':
|
||||
elif sub_type == 'atr':
|
||||
# card has been reset
|
||||
return CardReset(gsmtap_msg['body'])
|
||||
if sub_type in ['pps_req', 'pps_rsp']:
|
||||
elif sub_type in ['pps_req', 'pps_rsp']:
|
||||
# simply ignore for now
|
||||
pass
|
||||
else:
|
||||
@@ -86,3 +87,4 @@ class PysharkGsmtapPcap(_PysharkGsmtap):
|
||||
"""
|
||||
pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='gsm_sim', use_json=True, keep_packets=False)
|
||||
super().__init__(pyshark_inst)
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from pprint import pprint as pp
|
||||
from typing import Tuple
|
||||
import pyshark
|
||||
|
||||
from pySim.utils import h2b
|
||||
from pySim.utils import h2b, b2h
|
||||
from pySim.apdu import Tpdu
|
||||
from . import ApduSource, PacketType, CardReset
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# coding=utf-8
|
||||
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pySim.utils import h2b
|
||||
from pySim.gsmtap import 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
|
||||
|
||||
ApduCommands = UiccApduCommands + UiccAdmApduCommands + UsimApduCommands + GpApduCommands
|
||||
|
||||
class StdinHexApduSource(ApduSource):
|
||||
"""ApduSource for reading apdu hex-strings from stdin."""
|
||||
|
||||
def read_packet(self) -> PacketType:
|
||||
while True:
|
||||
command = input("C-APDU >")
|
||||
response = '9000'
|
||||
return ApduCommands.parse_cmd_bytes(h2b(command) + h2b(response))
|
||||
raise StopIteration
|
||||
@@ -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
|
||||
28
pySim/app.py
28
pySim/app.py
@@ -19,19 +19,18 @@ from typing import Tuple
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.filesystem import CardModel, CardApplication
|
||||
from pySim.cards import card_detect, SimCardBase, UiccCardBase, CardBase
|
||||
from pySim.cards import card_detect, SimCardBase, UiccCardBase
|
||||
from pySim.exceptions import NoCardError
|
||||
from pySim.runtime import RuntimeState
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.cdma_ruim import CardProfileRUIM
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.utils import all_subclasses
|
||||
from pySim.exceptions import SwMatchError
|
||||
|
||||
# we need to import this module so that the SysmocomSJA2 sub-class of
|
||||
# CardModel is created, which will add the ATR-based matching and
|
||||
# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
|
||||
import pySim.sysmocom_sja2
|
||||
#import pySim.sysmocom_euicc1
|
||||
|
||||
# we need to import these modules so that the various sub-classes of
|
||||
# CardProfile are created, which will be used in init_card() to iterate
|
||||
@@ -43,7 +42,7 @@ import pySim.ara_m
|
||||
import pySim.global_platform
|
||||
import pySim.euicc
|
||||
|
||||
def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState, SimCardBase]:
|
||||
def init_card(sl: LinkBase) -> Tuple[RuntimeState, SimCardBase]:
|
||||
"""
|
||||
Detect card in reader and setup card profile and runtime state. This
|
||||
function must be called at least once on startup. The card and runtime
|
||||
@@ -58,12 +57,6 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
|
||||
print("Waiting for card...")
|
||||
sl.wait_for_card(3)
|
||||
|
||||
# The user may opt to skip all card initialization. In this case only the
|
||||
# most basic card profile is selected. This mode is suitable for blank
|
||||
# cards that need card O/S initialization using APDU scripts first.
|
||||
if skip_card_init:
|
||||
return None, CardBase(scc)
|
||||
|
||||
generic_card = False
|
||||
card = card_detect(scc)
|
||||
if card is None:
|
||||
@@ -114,16 +107,7 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
|
||||
# inform the transport that we can do context-specific SW interpretation
|
||||
sl.set_sw_interpreter(rs)
|
||||
|
||||
# try to obtain the EID, if any
|
||||
isd_r = rs.mf.applications.get(pySim.euicc.AID_ISD_R.lower(), None)
|
||||
if isd_r:
|
||||
rs.lchan[0].select_file(isd_r)
|
||||
try:
|
||||
rs.identity['EID'] = pySim.euicc.CardApplicationISDR.get_eid(scc)
|
||||
except SwMatchError:
|
||||
# has ISD-R but not a SGP.22/SGP.32 eUICC - maybe SGP.02?
|
||||
pass
|
||||
finally:
|
||||
rs.reset()
|
||||
|
||||
return rs, card
|
||||
|
||||
|
||||
|
||||
|
||||
245
pySim/ara_m.py
245
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 osmocom.construct import *
|
||||
from osmocom.tlv import *
|
||||
from osmocom.utils import Hexstr
|
||||
from pySim.construct import *
|
||||
from pySim.filesystem import *
|
||||
import pySim.global_platform
|
||||
from pySim.tlv import *
|
||||
|
||||
# various BER-TLV encoded Data Objects (DOs)
|
||||
|
||||
|
||||
class AidRefDO(BER_TLV_IE, tag=0x4f):
|
||||
# GPD_SPE_013 v1.1 Table 6-3
|
||||
# SEID v1.1 Table 6-3
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
|
||||
class AidRefEmptyDO(BER_TLV_IE, tag=0xc0):
|
||||
# GPD_SPE_013 v1.1 Table 6-3
|
||||
# SEID v1.1 Table 6-3
|
||||
pass
|
||||
|
||||
|
||||
class DevAppIdRefDO(BER_TLV_IE, tag=0xc1):
|
||||
# GPD_SPE_013 v1.1 Table 6-4
|
||||
# SEID v1.1 Table 6-4
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
|
||||
@@ -58,39 +56,41 @@ class PkgRefDO(BER_TLV_IE, tag=0xca):
|
||||
|
||||
|
||||
class RefDO(BER_TLV_IE, tag=0xe1, nested=[AidRefDO, AidRefEmptyDO, DevAppIdRefDO, PkgRefDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-5
|
||||
# SEID v1.1 Table 6-5
|
||||
pass
|
||||
|
||||
|
||||
class ApduArDO(BER_TLV_IE, tag=0xd0):
|
||||
# GPD_SPE_013 v1.1 Table 6-8
|
||||
# SEID v1.1 Table 6-8
|
||||
def _from_bytes(self, do: bytes):
|
||||
if len(do) == 1:
|
||||
if do[0] == 0x00:
|
||||
self.decoded = {'generic_access_rule': 'never'}
|
||||
return self.decoded
|
||||
if do[0] == 0x01:
|
||||
elif do[0] == 0x01:
|
||||
self.decoded = {'generic_access_rule': 'always'}
|
||||
return self.decoded
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
if len(do) % 8:
|
||||
return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
|
||||
self.decoded = {'apdu_filter': []}
|
||||
self.decoded['apdu_filter'] = []
|
||||
offset = 0
|
||||
while offset < len(do):
|
||||
self.decoded['apdu_filter'] += [{'header': b2h(do[offset:offset+4]),
|
||||
'mask': b2h(do[offset+4:offset+8])}]
|
||||
offset += 8 # Move offset to the beginning of the next apdu_filter object
|
||||
return self.decoded
|
||||
self.decoded['apdu_filter'] += {'header': b2h(do[offset:offset+4]),
|
||||
'mask': b2h(do[offset+4:offset+8])}
|
||||
self.decoded = res
|
||||
return res
|
||||
|
||||
def _to_bytes(self):
|
||||
if 'generic_access_rule' in self.decoded:
|
||||
if self.decoded['generic_access_rule'] == 'never':
|
||||
return b'\x00'
|
||||
if self.decoded['generic_access_rule'] == 'always':
|
||||
elif self.decoded['generic_access_rule'] == 'always':
|
||||
return b'\x01'
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||
else:
|
||||
if not 'apdu_filter' in self.decoded:
|
||||
return ValueError('Invalid APDU AR DO')
|
||||
@@ -108,134 +108,135 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
|
||||
|
||||
|
||||
class NfcArDO(BER_TLV_IE, tag=0xd1):
|
||||
# GPD_SPE_013 v1.1 Table 6-9
|
||||
# SEID v1.1 Table 6-9
|
||||
_construct = Struct('nfc_event_access_rule' /
|
||||
Enum(Int8ub, never=0, always=1))
|
||||
|
||||
|
||||
class PermArDO(BER_TLV_IE, tag=0xdb):
|
||||
# Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
|
||||
# based on Table 6-8 of GlobalPlatform Device API Access Control v1.0
|
||||
_construct = Struct('permissions'/HexAdapter(Bytes(8)))
|
||||
|
||||
|
||||
class ArDO(BER_TLV_IE, tag=0xe3, nested=[ApduArDO, NfcArDO, PermArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-7
|
||||
# SEID v1.1 Table 6-7
|
||||
pass
|
||||
|
||||
|
||||
class RefArDO(BER_TLV_IE, tag=0xe2, nested=[RefDO, ArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-6
|
||||
# SEID v1.1 Table 6-6
|
||||
pass
|
||||
|
||||
|
||||
class ResponseAllRefArDO(BER_TLV_IE, tag=0xff40, nested=[RefArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 4-2
|
||||
# SEID v1.1 Table 4-2
|
||||
pass
|
||||
|
||||
|
||||
class ResponseArDO(BER_TLV_IE, tag=0xff50, nested=[ArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 4-3
|
||||
# SEID v1.1 Table 4-3
|
||||
pass
|
||||
|
||||
|
||||
class ResponseRefreshTagDO(BER_TLV_IE, tag=0xdf20):
|
||||
# GPD_SPE_013 v1.1 Table 4-4
|
||||
# SEID v1.1 Table 4-4
|
||||
_construct = Struct('refresh_tag'/HexAdapter(Bytes(8)))
|
||||
|
||||
|
||||
class DeviceInterfaceVersionDO(BER_TLV_IE, tag=0xe6):
|
||||
# GPD_SPE_013 v1.1 Table 6-12
|
||||
# SEID v1.1 Table 6-12
|
||||
_construct = Struct('major'/Int8ub, 'minor'/Int8ub, 'patch'/Int8ub)
|
||||
|
||||
|
||||
class DeviceConfigDO(BER_TLV_IE, tag=0xe4, nested=[DeviceInterfaceVersionDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-10
|
||||
# SEID v1.1 Table 6-10
|
||||
pass
|
||||
|
||||
|
||||
class ResponseDeviceConfigDO(BER_TLV_IE, tag=0xff7f, nested=[DeviceConfigDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-14
|
||||
# SEID v1.1 Table 5-14
|
||||
pass
|
||||
|
||||
|
||||
class AramConfigDO(BER_TLV_IE, tag=0xe5, nested=[DeviceInterfaceVersionDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-11
|
||||
# SEID v1.1 Table 6-11
|
||||
pass
|
||||
|
||||
|
||||
class ResponseAramConfigDO(BER_TLV_IE, tag=0xdf21, nested=[AramConfigDO]):
|
||||
# GPD_SPE_013 v1.1 Table 4-5
|
||||
# SEID v1.1 Table 4-5
|
||||
pass
|
||||
|
||||
|
||||
class CommandStoreRefArDO(BER_TLV_IE, tag=0xf0, nested=[RefArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-2
|
||||
# SEID v1.1 Table 5-2
|
||||
pass
|
||||
|
||||
|
||||
class CommandDelete(BER_TLV_IE, tag=0xf1, nested=[AidRefDO, AidRefEmptyDO, RefDO, RefArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-4
|
||||
# SEID v1.1 Table 5-4
|
||||
pass
|
||||
|
||||
|
||||
class CommandUpdateRefreshTagDO(BER_TLV_IE, tag=0xf2):
|
||||
# GPD_SPE_013 V1.1 Table 5-6
|
||||
# SEID V1.1 Table 5-6
|
||||
pass
|
||||
|
||||
|
||||
class CommandRegisterClientAidsDO(BER_TLV_IE, tag=0xf7, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-7
|
||||
# SEID v1.1 Table 5-7
|
||||
pass
|
||||
|
||||
|
||||
class CommandGet(BER_TLV_IE, tag=0xf3, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-8
|
||||
# SEID v1.1 Table 5-8
|
||||
pass
|
||||
|
||||
|
||||
class CommandGetAll(BER_TLV_IE, tag=0xf4):
|
||||
# GPD_SPE_013 v1.1 Table 5-9
|
||||
# SEID v1.1 Table 5-9
|
||||
pass
|
||||
|
||||
|
||||
class CommandGetClientAidsDO(BER_TLV_IE, tag=0xf6):
|
||||
# GPD_SPE_013 v1.1 Table 5-10
|
||||
# SEID v1.1 Table 5-10
|
||||
pass
|
||||
|
||||
|
||||
class CommandGetNext(BER_TLV_IE, tag=0xf5):
|
||||
# GPD_SPE_013 v1.1 Table 5-11
|
||||
# SEID v1.1 Table 5-11
|
||||
pass
|
||||
|
||||
|
||||
class CommandGetDeviceConfigDO(BER_TLV_IE, tag=0xf8):
|
||||
# GPD_SPE_013 v1.1 Table 5-12
|
||||
# SEID v1.1 Table 5-12
|
||||
pass
|
||||
|
||||
|
||||
class ResponseAracAidDO(BER_TLV_IE, tag=0xff70, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-13
|
||||
# SEID v1.1 Table 5-13
|
||||
pass
|
||||
|
||||
|
||||
class BlockDO(BER_TLV_IE, tag=0xe7):
|
||||
# GPD_SPE_013 v1.1 Table 6-13
|
||||
# SEID v1.1 Table 6-13
|
||||
_construct = Struct('offset'/Int16ub, 'length'/Int8ub)
|
||||
|
||||
|
||||
# GPD_SPE_013 v1.1 Table 4-1
|
||||
# SEID v1.1 Table 4-1
|
||||
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
|
||||
pass
|
||||
|
||||
# SEID v1.1 Table 4-2
|
||||
|
||||
|
||||
# GPD_SPE_013 v1.1 Table 4-2
|
||||
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
|
||||
ResponseRefreshTagDO, ResponseAramConfigDO]):
|
||||
pass
|
||||
|
||||
# SEID v1.1 Table 5-1
|
||||
|
||||
|
||||
# GPD_SPE_013 v1.1 Table 5-1
|
||||
class StoreCommandDoCollection(TLV_IE_Collection,
|
||||
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
|
||||
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
|
||||
@@ -244,7 +245,7 @@ class StoreCommandDoCollection(TLV_IE_Collection,
|
||||
pass
|
||||
|
||||
|
||||
# GPD_SPE_013 v1.1 Section 5.1.2
|
||||
# SEID v1.1 Section 5.1.2
|
||||
class StoreResponseDoCollection(TLV_IE_Collection,
|
||||
nested=[ResponseAllRefArDO, ResponseAracAidDO, ResponseDeviceConfigDO]):
|
||||
pass
|
||||
@@ -258,11 +259,8 @@ class ADF_ARAM(CardADF):
|
||||
files = []
|
||||
self.add_files(files)
|
||||
|
||||
def decode_select_response(self, data_hex):
|
||||
return pySim.global_platform.decode_select_response(data_hex)
|
||||
|
||||
@staticmethod
|
||||
def xceive_apdu_tlv(scc, hdr: Hexstr, cmd_do, resp_cls, exp_sw='9000'):
|
||||
def xceive_apdu_tlv(tp, hdr: Hexstr, cmd_do, resp_cls, exp_sw='9000'):
|
||||
"""Transceive an APDU with the card, transparently encoding the command data from TLV
|
||||
and decoding the response data tlv."""
|
||||
if cmd_do:
|
||||
@@ -274,43 +272,47 @@ class ADF_ARAM(CardADF):
|
||||
cmd_do_enc = b''
|
||||
cmd_do_len = 0
|
||||
c_apdu = hdr + ('%02x' % cmd_do_len) + b2h(cmd_do_enc)
|
||||
(data, _sw) = scc.send_apdu_checksw(c_apdu, exp_sw)
|
||||
(data, sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
|
||||
if data:
|
||||
if resp_cls:
|
||||
resp_do = resp_cls()
|
||||
resp_do.from_tlv(h2b(data))
|
||||
return resp_do
|
||||
return data
|
||||
else:
|
||||
return data
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def store_data(scc, do) -> bytes:
|
||||
def store_data(tp, do) -> bytes:
|
||||
"""Build the Command APDU for STORE DATA."""
|
||||
return ADF_ARAM.xceive_apdu_tlv(scc, '80e29000', do, StoreResponseDoCollection)
|
||||
return ADF_ARAM.xceive_apdu_tlv(tp, '80e29000', do, StoreResponseDoCollection)
|
||||
|
||||
@staticmethod
|
||||
def get_all(scc):
|
||||
return ADF_ARAM.xceive_apdu_tlv(scc, '80caff40', None, GetResponseDoCollection)
|
||||
def get_all(tp):
|
||||
return ADF_ARAM.xceive_apdu_tlv(tp, '80caff40', None, GetResponseDoCollection)
|
||||
|
||||
@staticmethod
|
||||
def get_config(scc, v_major=0, v_minor=0, v_patch=1):
|
||||
def get_config(tp, v_major=0, v_minor=0, v_patch=1):
|
||||
cmd_do = DeviceConfigDO()
|
||||
cmd_do.from_val_dict([{'device_interface_version_do': {
|
||||
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
|
||||
return ADF_ARAM.xceive_apdu_tlv(scc, '80cadf21', cmd_do, ResponseAramConfigDO)
|
||||
cmd_do.from_dict([{'device_interface_version_do': {
|
||||
'major': v_major, 'minor': v_minor, 'patch': v_patch}}])
|
||||
return ADF_ARAM.xceive_apdu_tlv(tp, '80cadf21', cmd_do, ResponseAramConfigDO)
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
def do_aram_get_all(self, _opts):
|
||||
def __init(self):
|
||||
super().__init__()
|
||||
|
||||
def do_aram_get_all(self, opts):
|
||||
"""GET DATA [All] on the ARA-M Applet"""
|
||||
res_do = ADF_ARAM.get_all(self._cmd.lchan.scc)
|
||||
res_do = ADF_ARAM.get_all(self._cmd.lchan.scc._tp)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
def do_aram_get_config(self, _opts):
|
||||
def do_aram_get_config(self, opts):
|
||||
"""Perform GET DATA [Config] on the ARA-M Applet: Tell it our version and retrieve its version."""
|
||||
res_do = ADF_ARAM.get_config(self._cmd.lchan.scc)
|
||||
res_do = ADF_ARAM.get_config(self._cmd.lchan.scc._tp)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
@@ -320,9 +322,9 @@ class ADF_ARAM(CardADF):
|
||||
'--device-app-id', required=True, help='Identifies the specific device application that the rule appplies to. Hash of Certificate of Application Provider, or UUID. (20/32 hex bytes)')
|
||||
aid_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||
aid_grp.add_argument(
|
||||
'--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 or 0 hex bytes)')
|
||||
'--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 hex bytes)')
|
||||
aid_grp.add_argument('--aid-empty', action='store_true',
|
||||
help='No specific SE application, applies to implicitly selected application (all channels)')
|
||||
help='No specific SE application, applies to all applications')
|
||||
store_ref_ar_do_parse.add_argument(
|
||||
'--pkg-ref', help='Full Android Java package name (up to 127 chars ASCII)')
|
||||
# AR-DO
|
||||
@@ -332,7 +334,7 @@ class ADF_ARAM(CardADF):
|
||||
apdu_grp.add_argument(
|
||||
'--apdu-always', action='store_true', help='APDU access is allowed')
|
||||
apdu_grp.add_argument(
|
||||
'--apdu-filter', help='APDU filter: multiple groups of 8 hex bytes (4 byte CLA/INS/P1/P2 followed by 4 byte mask)')
|
||||
'--apdu-filter', help='APDU filter: 4 byte CLA/INS/P1/P2 followed by 4 byte mask (8 hex bytes)')
|
||||
nfc_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||
nfc_grp.add_argument('--nfc-always', action='store_true',
|
||||
help='NFC event access is allowed')
|
||||
@@ -346,7 +348,7 @@ class ADF_ARAM(CardADF):
|
||||
"""Perform STORE DATA [Command-Store-REF-AR-DO] to store a (new) access rule."""
|
||||
# REF
|
||||
ref_do_content = []
|
||||
if opts.aid is not None:
|
||||
if opts.aid:
|
||||
ref_do_content += [{'aid_ref_do': opts.aid}]
|
||||
elif opts.aid_empty:
|
||||
ref_do_content += [{'aid_ref_empty_do': None}]
|
||||
@@ -356,19 +358,12 @@ class ADF_ARAM(CardADF):
|
||||
# AR
|
||||
ar_do_content = []
|
||||
if opts.apdu_never:
|
||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'never'}}]
|
||||
ar_do_content += [{'apdu_ar_od': {'generic_access_rule': 'never'}}]
|
||||
elif opts.apdu_always:
|
||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
|
||||
elif opts.apdu_filter:
|
||||
if len(opts.apdu_filter) % 16:
|
||||
return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do))
|
||||
offset = 0
|
||||
apdu_filter = []
|
||||
while offset < len(opts.apdu_filter):
|
||||
apdu_filter += [{'header': opts.apdu_filter[offset:offset+8],
|
||||
'mask': opts.apdu_filter[offset+8:offset+16]}]
|
||||
offset += 16 # Move offset to the beginning of the next apdu_filter object
|
||||
ar_do_content += [{'apdu_ar_do': {'apdu_filter': apdu_filter}}]
|
||||
# TODO: multiple filters
|
||||
ar_do_content += [{'apdu_ar_do': {'apdu_filter': [opts.apdu_filter]}}]
|
||||
if opts.nfc_always:
|
||||
ar_do_content += [{'nfc_ar_do': {'nfc_event_access_rule': 'always'}}]
|
||||
elif opts.nfc_never:
|
||||
@@ -377,23 +372,18 @@ class ADF_ARAM(CardADF):
|
||||
ar_do_content += [{'perm_ar_do': {'permissions': opts.android_permissions}}]
|
||||
d = [{'ref_ar_do': [{'ref_do': ref_do_content}, {'ar_do': ar_do_content}]}]
|
||||
csrado = CommandStoreRefArDO()
|
||||
csrado.from_val_dict(d)
|
||||
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, csrado)
|
||||
csrado.from_dict(d)
|
||||
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, csrado)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
def do_aram_delete_all(self, _opts):
|
||||
def do_aram_delete_all(self, opts):
|
||||
"""Perform STORE DATA [Command-Delete[all]] to delete all access rules."""
|
||||
deldo = CommandDelete()
|
||||
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc, deldo)
|
||||
res_do = ADF_ARAM.store_data(self._cmd.lchan.scc._tp, deldo)
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
def do_aram_lock(self, opts):
|
||||
"""Lock STORE DATA command to prevent unauthorized changes
|
||||
(Proprietary feature that is specific to sysmocom's fork of Bertrand 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
|
||||
sw_aram = {
|
||||
@@ -420,84 +410,3 @@ sw_aram = {
|
||||
class CardApplicationARAM(CardApplication):
|
||||
def __init__(self):
|
||||
super().__init__('ARA-M', adf=ADF_ARAM(), sw=sw_aram)
|
||||
|
||||
@staticmethod
|
||||
def __export_get_from_dictlist(key, dictlist):
|
||||
# Data objects are organized in lists that contain dictionaries, usually there is only one dictionary per
|
||||
# list item. This function goes through that list and gets the value of the first dictionary that has the
|
||||
# matching key.
|
||||
if dictlist is None:
|
||||
return None
|
||||
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/>.
|
||||
#
|
||||
|
||||
from pySim.transport import LinkBase
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
from pySim.transport import LinkBase
|
||||
|
||||
class CardHandlerBase:
|
||||
"""Abstract base class representing a mechanism for card insertion/removal."""
|
||||
@@ -96,7 +97,7 @@ class CardHandlerAuto(CardHandlerBase):
|
||||
print("Card handler Config-file: " + str(config_file))
|
||||
with open(config_file) as cfg:
|
||||
self.cmds = yaml.load(cfg, Loader=yaml.FullLoader)
|
||||
self.verbose = self.cmds.get('verbose') is True
|
||||
self.verbose = (self.cmds.get('verbose') == True)
|
||||
|
||||
def __print_outout(self, out):
|
||||
print("")
|
||||
|
||||
@@ -10,10 +10,10 @@ the need of manually entering the related card-individual data on every
|
||||
operation with pySim-shell.
|
||||
"""
|
||||
|
||||
# (C) 2021-2024 by Sysmocom s.f.m.c. GmbH
|
||||
# (C) 2021 by Sysmocom s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Philipp Maier, Harald Welte
|
||||
# Author: Philipp Maier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -29,29 +29,18 @@ operation with pySim-shell.
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import List, Dict, Optional
|
||||
from Cryptodome.Cipher import AES
|
||||
from osmocom.utils import h2b, b2h
|
||||
|
||||
import abc
|
||||
import csv
|
||||
|
||||
card_key_providers = [] # type: List['CardKeyProvider']
|
||||
|
||||
# well-known groups of columns relate to a given functionality. This avoids having
|
||||
# to specify the same transport key N number of times, if the same key is used for multiple
|
||||
# fields of one group, like KIC+KID+KID of one SD.
|
||||
CRYPT_GROUPS = {
|
||||
'UICC_SCP02': ['UICC_SCP02_KIC1', 'UICC_SCP02_KID1', 'UICC_SCP02_KIK1'],
|
||||
'UICC_SCP03': ['UICC_SCP03_KIC1', 'UICC_SCP03_KID1', 'UICC_SCP03_KIK1'],
|
||||
'SCP03_ISDR': ['SCP03_ENC_ISDR', 'SCP03_MAC_ISDR', 'SCP03_DEK_ISDR'],
|
||||
'SCP03_ISDA': ['SCP03_ENC_ISDR', 'SCP03_MAC_ISDA', 'SCP03_DEK_ISDA'],
|
||||
'SCP03_ECASD': ['SCP03_ENC_ECASD', 'SCP03_MAC_ECASD', 'SCP03_DEK_ECASD'],
|
||||
}
|
||||
|
||||
class CardKeyProvider(abc.ABC):
|
||||
"""Base class, not containing any concrete implementation."""
|
||||
|
||||
VALID_KEY_FIELD_NAMES = ['ICCID', 'EID', 'IMSI' ]
|
||||
VALID_FIELD_NAMES = ['ICCID', 'ADM1',
|
||||
'IMSI', 'PIN1', 'PIN2', 'PUK1', 'PUK2']
|
||||
|
||||
# check input parameters, but do nothing concrete yet
|
||||
def _verify_get_data(self, fields: List[str] = [], key: str = 'ICCID', value: str = "") -> Dict[str, str]:
|
||||
@@ -64,10 +53,14 @@ class CardKeyProvider(abc.ABC):
|
||||
Returns:
|
||||
dictionary of {field, value} strings for each requested field from 'fields'
|
||||
"""
|
||||
for f in fields:
|
||||
if (f not in self.VALID_FIELD_NAMES):
|
||||
raise ValueError("Requested field name '%s' is not a valid field name, valid field names are: %s" %
|
||||
(f, str(self.VALID_FIELD_NAMES)))
|
||||
|
||||
if key not in self.VALID_KEY_FIELD_NAMES:
|
||||
if (key not in self.VALID_FIELD_NAMES):
|
||||
raise ValueError("Key field name '%s' is not a valid field name, valid field names are: %s" %
|
||||
(key, str(self.VALID_KEY_FIELD_NAMES)))
|
||||
(key, str(self.VALID_FIELD_NAMES)))
|
||||
|
||||
return {}
|
||||
|
||||
@@ -91,47 +84,19 @@ class CardKeyProvider(abc.ABC):
|
||||
|
||||
|
||||
class CardKeyProviderCsv(CardKeyProvider):
|
||||
"""Card key provider implementation that allows to query against a specified CSV file.
|
||||
Supports column-based encryption as it is generally a bad idea to store cryptographic key material in
|
||||
plaintext. Instead, the key material should be encrypted by a "key-encryption key", occasionally also
|
||||
known as "transport key" (see GSMA FS.28)."""
|
||||
IV = b'\x23' * 16
|
||||
"""Card key provider implementation that allows to query against a specified CSV file"""
|
||||
csv_file = None
|
||||
filename = None
|
||||
|
||||
def __init__(self, filename: str, transport_keys: dict):
|
||||
def __init__(self, filename: str):
|
||||
"""
|
||||
Args:
|
||||
filename : file name (path) of CSV file containing card-individual key/data
|
||||
transport_keys : a dict indexed by field name, whose values are hex-encoded AES keys for the
|
||||
respective field (column) of the CSV. This is done so that different fields
|
||||
(columns) can use different transport keys, which is strongly recommended by
|
||||
GSMA FS.28
|
||||
"""
|
||||
self.csv_file = open(filename, 'r')
|
||||
if not self.csv_file:
|
||||
raise RuntimeError("Could not open CSV file '%s'" % filename)
|
||||
self.filename = filename
|
||||
self.transport_keys = self.process_transport_keys(transport_keys)
|
||||
|
||||
@staticmethod
|
||||
def process_transport_keys(transport_keys: dict):
|
||||
"""Apply a single transport key to multiple fields/columns, if the name is a group."""
|
||||
new_dict = {}
|
||||
for name, key in transport_keys.items():
|
||||
if name in CRYPT_GROUPS:
|
||||
for field in CRYPT_GROUPS[name]:
|
||||
new_dict[field] = key
|
||||
else:
|
||||
new_dict[name] = key
|
||||
return new_dict
|
||||
|
||||
def _decrypt_field(self, field_name: str, encrypted_val: str) -> str:
|
||||
"""decrypt a single field, if we have a transport key for the field of that name."""
|
||||
if not field_name in self.transport_keys:
|
||||
return encrypted_val
|
||||
cipher = AES.new(h2b(self.transport_keys[field_name]), AES.MODE_CBC, self.IV)
|
||||
return b2h(cipher.decrypt(h2b(encrypted_val)))
|
||||
|
||||
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
|
||||
super()._verify_get_data(fields, key, value)
|
||||
@@ -148,7 +113,7 @@ class CardKeyProviderCsv(CardKeyProvider):
|
||||
if row[key] == value:
|
||||
for f in fields:
|
||||
if f in row:
|
||||
rc.update({f: self._decrypt_field(f, row[f])})
|
||||
rc.update({f: row[f]})
|
||||
else:
|
||||
raise RuntimeError("CSV-File '%s' lacks column '%s'" %
|
||||
(self.filename, f))
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from typing import Optional, Tuple
|
||||
from osmocom.utils import *
|
||||
|
||||
from pySim.ts_102_221 import EF_DIR, CardProfileUICC
|
||||
from typing import Optional, Dict, Tuple
|
||||
from pySim.ts_102_221 import EF_DIR
|
||||
from pySim.ts_51_011 import DF_GSM
|
||||
from pySim.utils import SwHexstr
|
||||
import abc
|
||||
|
||||
from pySim.utils import *
|
||||
from pySim.commands import Path, SimCardCommands
|
||||
|
||||
class CardBase:
|
||||
@@ -40,7 +40,8 @@ class CardBase:
|
||||
rc = self._scc.reset_card()
|
||||
if rc == 1:
|
||||
return self._scc.get_atr()
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_apdu_parameter(self, cla: Hexstr, sel_ctrl: Hexstr) -> None:
|
||||
"""Set apdu parameters (class byte and selection control bytes)"""
|
||||
@@ -53,19 +54,13 @@ class CardBase:
|
||||
|
||||
def erase(self):
|
||||
print("warning: erasing is not supported for specified card type!")
|
||||
return
|
||||
|
||||
def file_exists(self, fid: Path) -> bool:
|
||||
"""Determine if the file exists (and is not deactivated)."""
|
||||
res_arr = self._scc.try_select_path(fid)
|
||||
for res in res_arr:
|
||||
if res[1] != '9000':
|
||||
return False
|
||||
try:
|
||||
d = CardProfileUICC.decode_select_response(res_arr[-1][0])
|
||||
if d.get('life_cycle_status_integer', 'operational_activated') != 'operational_activated':
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
return True
|
||||
|
||||
def read_aids(self) -> List[Hexstr]:
|
||||
@@ -73,16 +68,6 @@ class CardBase:
|
||||
# callers having to do hasattr('read_aids') ahead of every call.
|
||||
return []
|
||||
|
||||
def adf_present(self, adf: str = "usim") -> bool:
|
||||
# a non-UICC doesn't have any applications. Convenience helper to avoid
|
||||
# callers having to do hasattr('adf_present') ahead of every call.
|
||||
return False
|
||||
|
||||
def select_adf_by_aid(self, adf: str = "usim", scc: Optional[SimCardCommands] = None) -> Tuple[Optional[Hexstr], Optional[SwHexstr]]:
|
||||
# a non-UICC doesn't have any applications. Convenience helper to avoid
|
||||
# callers having to do hasattr('select_adf_by_aid') ahead of every call.
|
||||
return (None, None)
|
||||
|
||||
|
||||
class SimCardBase(CardBase):
|
||||
"""Here we only add methods for commands specified in TS 51.011, without
|
||||
@@ -90,7 +75,7 @@ class SimCardBase(CardBase):
|
||||
name = 'SIM'
|
||||
|
||||
def __init__(self, scc: SimCardCommands):
|
||||
super().__init__(scc)
|
||||
super(SimCardBase, self).__init__(scc)
|
||||
self._scc.cla_byte = "A0"
|
||||
self._scc.sel_ctrl = "0000"
|
||||
|
||||
@@ -103,7 +88,7 @@ class UiccCardBase(SimCardBase):
|
||||
name = 'UICC'
|
||||
|
||||
def __init__(self, scc: SimCardCommands):
|
||||
super().__init__(scc)
|
||||
super(UiccCardBase, self).__init__(scc)
|
||||
self._scc.cla_byte = "00"
|
||||
self._scc.sel_ctrl = "0004" # request an FCP
|
||||
# See also: ETSI TS 102 221, Table 9.3
|
||||
@@ -173,8 +158,9 @@ class UiccCardBase(SimCardBase):
|
||||
aid_full = self._complete_aid(aid)
|
||||
if aid_full:
|
||||
return scc.select_adf(aid_full)
|
||||
# If we cannot get the full AID, try with short AID
|
||||
return scc.select_adf(aid)
|
||||
else:
|
||||
# If we cannot get the full AID, try with short AID
|
||||
return scc.select_adf(aid)
|
||||
return (None, None)
|
||||
|
||||
def card_detect(scc: SimCardCommands) -> Optional[CardBase]:
|
||||
|
||||
406
pySim/cat.py
406
pySim/cat.py
@@ -2,7 +2,7 @@
|
||||
mainly) ETSI TS 102 223, ETSI TS 101 220 and USIM Application Toolkit (SAT)
|
||||
as described in 3GPP TS 31.111."""
|
||||
|
||||
# (C) 2021-2024 by Harald Welte <laforge@osmocom.org>
|
||||
# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -18,58 +18,43 @@ as described in 3GPP TS 31.111."""
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from typing import List
|
||||
from bidict import bidict
|
||||
from construct import Int8ub, Int16ub, Byte, BitsInteger
|
||||
from construct import Struct, Enum, BitStruct, this
|
||||
from construct import Switch, GreedyRange, FlagsEnum
|
||||
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
|
||||
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
|
||||
from osmocom.utils import b2h, h2b
|
||||
from pySim.utils import dec_xplmn_w_act
|
||||
from typing import List
|
||||
from pySim.utils import b2h, h2b, dec_xplmn_w_act
|
||||
from pySim.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
|
||||
from pySim.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi
|
||||
from construct import Int8ub, Int16ub, Byte, Bytes, Bit, Flag, BitsInteger
|
||||
from construct import Struct, Enum, Tell, BitStruct, this, Padding, RepeatUntil
|
||||
from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
|
||||
|
||||
# Tag values as per TS 101 220 Table 7.23
|
||||
|
||||
# TS 102 223 Section 8.1
|
||||
class Address(COMPR_TLV_IE, tag=0x86):
|
||||
_construct = Struct('ton_npi'/TonNpi,
|
||||
'call_number'/BcdAdapter(GreedyBytes))
|
||||
class Address(COMPR_TLV_IE, tag=0x06):
|
||||
_construct = Struct('ton_npi'/Int8ub,
|
||||
'call_number'/BcdAdapter(Bytes(this._.total_len-1)))
|
||||
|
||||
# TS 102 223 Section 8.2
|
||||
class AlphaIdentifier(COMPR_TLV_IE, tag=0x85):
|
||||
class AlphaIdentifier(COMPR_TLV_IE, tag=0x05):
|
||||
# FIXME: like EF.ADN
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 8.3
|
||||
class Subaddress(COMPR_TLV_IE, tag=0x88):
|
||||
class Subaddress(COMPR_TLV_IE, tag=0x08):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 8.4 + TS 31.111 Section 8.4
|
||||
class CapabilityConfigParams(COMPR_TLV_IE, tag=0x87):
|
||||
class CapabilityConfigParams(COMPR_TLV_IE, tag=0x07):
|
||||
pass
|
||||
|
||||
# TS 31.111 Section 8.5
|
||||
class CBSPage(COMPR_TLV_IE, tag=0x8C):
|
||||
class CBSPage(COMPR_TLV_IE, tag=0x0C):
|
||||
pass
|
||||
|
||||
# TS 102 223 V15.3.0 Section 9.4
|
||||
TypeOfCommand = Enum(Int8ub, refresh=0x01, more_time=0x02, poll_interval=0x03, polling_off=0x04,
|
||||
set_up_event_list=0x05, set_up_call=0x10, send_ss=0x11, send_ussd=0x12,
|
||||
send_short_message=0x13, send_dtmf=0x14, launch_browser=0x15, geo_location_req=0x16,
|
||||
play_tone=0x20, display_text=0x21, get_inkey=0x22, get_input=0x23, select_item=0x24,
|
||||
set_up_menu=0x25, provide_local_info=0x26, timer_management=0x27,
|
||||
set_up_idle_mode_text=0x28, perform_card_apdu=0x30, power_on_card=0x31,
|
||||
power_off_card=0x32, get_reader_status=0x33, run_at_command=0x34,
|
||||
language_notification=0x35, open_channel=0x40, close_channel=0x41, receive_data=0x42,
|
||||
send_data=0x43, get_channel_status=0x44, service_search=0x45, get_service_info=0x46,
|
||||
declare_service=0x47, set_frames=0x50, get_frames_status=0x51, retrieve_mms=0x60,
|
||||
submit_mms=0x61, display_mms=0x62, activate=0x70, contactless_state_changed=0x71,
|
||||
command_container=0x72, encapsulated_session_control=0x73)
|
||||
|
||||
# TS 102 223 Section 8.6 + TS 31.111 Section 8.6
|
||||
class CommandDetails(COMPR_TLV_IE, tag=0x81):
|
||||
_construct = Struct('command_number'/Int8ub,
|
||||
'type_of_command'/TypeOfCommand,
|
||||
'type_of_command'/Int8ub,
|
||||
'command_qualifier'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.7
|
||||
@@ -122,26 +107,26 @@ class DeviceIdentities(COMPR_TLV_IE, tag=0x82):
|
||||
return bytes([src, dst])
|
||||
|
||||
# TS 102 223 Section 8.8
|
||||
class Duration(COMPR_TLV_IE, tag=0x84):
|
||||
class Duration(COMPR_TLV_IE, tag=0x04):
|
||||
_construct = Struct('time_unit'/Enum(Int8ub, minutes=0, seconds=1, tenths_of_seconds=2),
|
||||
'time_interval'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.9
|
||||
class Item(COMPR_TLV_IE, tag=0x8f):
|
||||
class Item(COMPR_TLV_IE, tag=0x0f):
|
||||
_construct = Struct('identifier'/Int8ub,
|
||||
'text_string'/GsmStringAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.10
|
||||
class ItemIdentifier(COMPR_TLV_IE, tag=0x90):
|
||||
class ItemIdentifier(COMPR_TLV_IE, tag=0x10):
|
||||
_construct = Struct('identifier'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.11
|
||||
class ResponseLength(COMPR_TLV_IE, tag=0x91):
|
||||
class ResponseLength(COMPR_TLV_IE, tag=0x11):
|
||||
_construct = Struct('minimum_length'/Int8ub,
|
||||
'maximum_length'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.12
|
||||
class Result(COMPR_TLV_IE, tag=0x83):
|
||||
class Result(COMPR_TLV_IE, tag=0x03):
|
||||
GeneralResult = Enum(Int8ub,
|
||||
# '0X' and '1X' indicate that the command has been performed
|
||||
performed_successfully=0,
|
||||
@@ -255,27 +240,24 @@ class Result(COMPR_TLV_IE, tag=0x83):
|
||||
'launch_browser_generic_error': AddlInfoLaunchBrowser,
|
||||
'bearer_independent_protocol_error': AddlInfoBip,
|
||||
'frames_error': AddlInfoFrames
|
||||
}, default=GreedyBytes))
|
||||
}, default=HexAdapter(GreedyBytes)))
|
||||
|
||||
# TS 102 223 Section 8.13 + TS 31.111 Section 8.13
|
||||
class SMS_TPDU(COMPR_TLV_IE, tag=0x8B):
|
||||
_construct = Struct('tpdu'/GreedyBytes)
|
||||
_construct = Struct('tpdu'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 31.111 Section 8.14
|
||||
class SsString(COMPR_TLV_IE, tag=0x89):
|
||||
_construct = Struct('ton_npi'/TonNpi, 'ss_string'/GreedyBytes)
|
||||
_construct = Struct('ton_npi'/TonNpi, 'ss_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
|
||||
# TS 102 223 Section 8.15
|
||||
class TextString(COMPR_TLV_IE, tag=0x8D):
|
||||
_test_de_encode = [
|
||||
( '8d090470617373776f7264', {'dcs': 4, 'text_string': b'password'} )
|
||||
]
|
||||
class TextString(COMPR_TLV_IE, tag=0x0d):
|
||||
_construct = Struct('dcs'/Int8ub, # TS 03.38
|
||||
'text_string'/GreedyBytes)
|
||||
'text_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.16
|
||||
class Tone(COMPR_TLV_IE, tag=0x8E):
|
||||
class Tone(COMPR_TLV_IE, tag=0x0e):
|
||||
_construct = Struct('tone'/Enum(Int8ub, dial_tone=0x01,
|
||||
called_subscriber_busy=0x02,
|
||||
congestion=0x03,
|
||||
@@ -306,13 +288,13 @@ class Tone(COMPR_TLV_IE, tag=0x8E):
|
||||
melody_8=0x47))
|
||||
|
||||
# TS 31 111 Section 8.17
|
||||
class USSDString(COMPR_TLV_IE, tag=0x8A):
|
||||
class USSDString(COMPR_TLV_IE, tag=0x0a):
|
||||
_construct = Struct('dcs'/Int8ub,
|
||||
'ussd_string'/GreedyBytes)
|
||||
'ussd_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.18
|
||||
class FileList(COMPR_TLV_IE, tag=0x92):
|
||||
FileId=Bytes(2)
|
||||
class FileList(COMPR_TLV_IE, tag=0x12):
|
||||
FileId=HexAdapter(Bytes(2))
|
||||
_construct = Struct('number_of_files'/Int8ub,
|
||||
'files'/GreedyRange(FileId))
|
||||
|
||||
@@ -335,10 +317,10 @@ class NetworkMeasurementResults(COMPR_TLV_IE, tag=0x96):
|
||||
# TS 102 223 Section 8.23
|
||||
class DefaultText(COMPR_TLV_IE, tag=0x97):
|
||||
_construct = Struct('dcs'/Int8ub,
|
||||
'text_string'/GreedyBytes)
|
||||
'text_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.24
|
||||
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x98):
|
||||
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x18):
|
||||
_construct = GreedyRange(Int8ub)
|
||||
|
||||
class EventList(COMPR_TLV_IE, tag=0x99):
|
||||
@@ -383,7 +365,7 @@ class LocationStatus(COMPR_TLV_IE, tag=0x9b):
|
||||
_construct = Enum(Int8ub, normal_service=0, limited_service=1, no_service=2)
|
||||
|
||||
# TS 102 223 Section 8.31
|
||||
class IconIdentifier(COMPR_TLV_IE, tag=0x9e):
|
||||
class IconIdentifier(COMPR_TLV_IE, tag=0x1e):
|
||||
_construct = Struct('icon_qualifier'/FlagsEnum(Int8ub, not_self_explanatory=1),
|
||||
'icon_identifier'/Int8ub)
|
||||
|
||||
@@ -394,7 +376,7 @@ class ItemIconIdentifierList(COMPR_TLV_IE, tag=0x9f):
|
||||
|
||||
# TS 102 223 Section 8.35
|
||||
class CApdu(COMPR_TLV_IE, tag=0xA2):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.37
|
||||
class TimerIdentifier(COMPR_TLV_IE, tag=0xA4):
|
||||
@@ -406,51 +388,28 @@ class TimerValue(COMPR_TLV_IE, tag=0xA5):
|
||||
|
||||
# TS 102 223 Section 8.40
|
||||
class AtCommand(COMPR_TLV_IE, tag=0xA8):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.43
|
||||
class ImmediateResponse(COMPR_TLV_IE, tag=0xAB):
|
||||
class ImmediateResponse(COMPR_TLV_IE, tag=0x2b):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 8.44
|
||||
class DtmfString(COMPR_TLV_IE, tag=0xAC):
|
||||
_construct = BcdAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.45
|
||||
class Language(COMPR_TLV_IE, tag=0xAD):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 31.111 Section 8.46
|
||||
class TimingAdvance(COMPR_TLV_IE, tag=0xC6):
|
||||
class TimingAdvance(COMPR_TLV_IE, tag=0x46):
|
||||
_construct = Struct('me_status'/Enum(Int8ub, in_idle_state=0, not_in_idle_state=1),
|
||||
'timing_advance'/Int8ub)
|
||||
|
||||
# TS 31.111 Section 8.47
|
||||
class BrowserIdentity(COMPR_TLV_IE, tag=0xB0):
|
||||
_construct = Enum(Int8ub, default=0, wml=1, html=2, xhtml=3, chtml=4)
|
||||
|
||||
# TS 31.111 Section 8.48
|
||||
class Url(COMPR_TLV_IE, tag=0xB1):
|
||||
_construct = GsmString(GreedyBytes)
|
||||
|
||||
# TS 31.111 Section 8.49
|
||||
class Bearer(COMPR_TLV_IE, tag=0xB2):
|
||||
SingleBearer = Enum(Int8ub, sms=0, csd=1, ussd=2, packet_Service=3)
|
||||
_construct = GreedyRange(SingleBearer)
|
||||
|
||||
# TS 102 223 Section 8.50
|
||||
class ProvisioningFileReference(COMPR_TLV_IE, tag=0xB3):
|
||||
_construct = GreedyBytes
|
||||
|
||||
# TS 102 223 Section 8.51
|
||||
class BrowserTerminationCause(COMPR_TLV_IE, tag=0xB4):
|
||||
_construct = Enum(Int8ub, user_termination=0, error_termination=1)
|
||||
|
||||
# TS 102 223 Section 8.52
|
||||
class BearerDescription(COMPR_TLV_IE, tag=0xB5):
|
||||
_test_de_encode = [
|
||||
( 'b50103', {'bearer_parameters': b'', 'bearer_type': 'default'} ),
|
||||
]
|
||||
# TS 31.111 Section 8.52.1
|
||||
BearerParsCs = Struct('data_rate'/Int8ub,
|
||||
'bearer_service'/Int8ub,
|
||||
@@ -492,11 +451,11 @@ class BearerDescription(COMPR_TLV_IE, tag=0xB5):
|
||||
'packet_grps_utran_eutran': BearerParsPacket,
|
||||
'packet_with_extd_params': BearerParsPacketExt,
|
||||
'ng_ran': BearerParsNgRan,
|
||||
}, default=GreedyBytes))
|
||||
}, default=HexAdapter(GreedyBytes)))
|
||||
|
||||
# TS 102 223 Section 8.53
|
||||
class ChannelData(COMPR_TLV_IE, tag = 0xB6):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.54
|
||||
class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
|
||||
@@ -506,33 +465,26 @@ class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
|
||||
class BufferSize(COMPR_TLV_IE, tag = 0xB9):
|
||||
_construct = Int16ub
|
||||
|
||||
# TS 102 223 Section 8.56 + TS 31.111 Section 8.56
|
||||
# TS 31.111 Section 8.56
|
||||
class ChannelStatus(COMPR_TLV_IE, tag = 0xB8):
|
||||
# complex decoding, depends on out-of-band context/knowledge :(
|
||||
# for default / TCP Client mode: bit 8 of first byte indicates connected, 3 LSB indicate channel nr
|
||||
_construct = GreedyBytes
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 8.58
|
||||
class OtherAddress(COMPR_TLV_IE, tag = 0xBE):
|
||||
_test_de_encode = [
|
||||
( 'be052101020304', {'address': h2b('01020304'), 'type_of_address': 'ipv4'} ),
|
||||
]
|
||||
_construct = Struct('type_of_address'/Enum(Int8ub, ipv4=0x21, ipv6=0x57),
|
||||
'address'/GreedyBytes)
|
||||
'address'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.59
|
||||
class UiccTransportLevel(COMPR_TLV_IE, tag = 0xBC):
|
||||
_test_de_encode = [
|
||||
( 'bc03028000', {'port_number': 32768, 'protocol_type': 'tcp_uicc_client_remote'} ),
|
||||
]
|
||||
_construct = Struct('protocol_type'/Enum(Int8ub, udp_uicc_client_remote=1, tcp_uicc_client_remote=2,
|
||||
tcp_uicc_server=3, udp_uicc_client_local=4,
|
||||
tcp_uicc_client_local=5, direct_channel=6),
|
||||
'port_number'/Int16ub)
|
||||
|
||||
# TS 102 223 Section 8.60
|
||||
class Aid(COMPR_TLV_IE, tag=0xAF):
|
||||
_construct = Struct('aid'/GreedyBytes)
|
||||
class Aid(COMPR_TLV_IE, tag=0x2f):
|
||||
_construct = Struct('aid'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.61
|
||||
class AccessTechnology(COMPR_TLV_IE, tag=0xBF):
|
||||
@@ -546,38 +498,35 @@ class ServiceRecord(COMPR_TLV_IE, tag=0xC1):
|
||||
BearerTechId = Enum(Int8ub, technology_independent=0, bluetooth=1, irda=2, rs232=3, usb=4)
|
||||
_construct = Struct('local_bearer_technology'/BearerTechId,
|
||||
'service_identifier'/Int8ub,
|
||||
'service_record'/GreedyBytes)
|
||||
'service_record'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.64
|
||||
class DeviceFilter(COMPR_TLV_IE, tag=0xC2):
|
||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||
'device_filter'/GreedyBytes)
|
||||
'device_filter'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.65
|
||||
class ServiceSearchIE(COMPR_TLV_IE, tag=0xC3):
|
||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||
'service_search'/GreedyBytes)
|
||||
'service_search'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.66
|
||||
class AttributeInformation(COMPR_TLV_IE, tag=0xC4):
|
||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||
'attribute_information'/GreedyBytes)
|
||||
'attribute_information'/HexAdapter(GreedyBytes))
|
||||
|
||||
|
||||
# TS 102 223 Section 8.68
|
||||
class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
|
||||
_construct = Struct('coding_type'/Enum(Int8ub, ieee802_16=0, irda=1),
|
||||
'address'/GreedyBytes)
|
||||
'address'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.70
|
||||
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
|
||||
_test_de_encode = [
|
||||
( 'c704036e6161', h2b('036e6161') ),
|
||||
]
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.72
|
||||
class TextAttribute(COMPR_TLV_IE, tag=0xD0):
|
||||
class TextAttribute(COMPR_TLV_IE, tag=0x50):
|
||||
pass
|
||||
|
||||
# TS 31.111 Section 8.72
|
||||
@@ -613,20 +562,20 @@ class ItemTextAttributeList(COMPR_TLV_IE, tag=0xD1):
|
||||
_construct = GreedyRange(Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.80
|
||||
class FrameIdentifier(COMPR_TLV_IE, tag=0xE8):
|
||||
class FrameIdentifier(COMPR_TLV_IE, tag=0x68):
|
||||
_construct = Struct('identifier'/Int8ub)
|
||||
|
||||
# TS 102 223 Section 8.82
|
||||
class MultimediaMessageReference(COMPR_TLV_IE, tag=0xEA):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.83
|
||||
class MultimediaMessageIdentifier(COMPR_TLV_IE, tag=0xEB):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.85
|
||||
class MmContentIdentifier(COMPR_TLV_IE, tag=0xEE):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.89
|
||||
class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
|
||||
@@ -634,11 +583,11 @@ class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
|
||||
|
||||
# TS 31.111 Section 8.90
|
||||
class PlmnWactList(COMPR_TLV_IE, tag=0xF2):
|
||||
def _from_bytes(self, do: bytes):
|
||||
def _from_bytes(self, x):
|
||||
r = []
|
||||
i = 0
|
||||
while i < len(do):
|
||||
r.append(dec_xplmn_w_act(b2h(do[i:i+5])))
|
||||
while i < len(x):
|
||||
r.append(dec_xplmn_w_act(b2h(x[i:i+5])))
|
||||
i += 5
|
||||
return r
|
||||
|
||||
@@ -649,7 +598,7 @@ class ContactlessFunctionalityState(COMPR_TLV_IE, tag=0xD4):
|
||||
# TS 31.111 Section 8.91
|
||||
class RoutingAreaIdentification(COMPR_TLV_IE, tag=0xF3):
|
||||
_construct = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
|
||||
'lac'/Bytes(2),
|
||||
'lac'/HexAdapter(Bytes(2)),
|
||||
'rac'/Int8ub)
|
||||
|
||||
# TS 31.111 Section 8.92
|
||||
@@ -709,23 +658,23 @@ class EcatSequenceNumber(COMPR_TLV_IE, tag=0xA1):
|
||||
|
||||
# TS 102 223 Section 8.99
|
||||
class EncryptedTlvList(COMPR_TLV_IE, tag=0xA2):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.100
|
||||
class Mac(COMPR_TLV_IE, tag=0xE0):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.101
|
||||
class SaTemplate(COMPR_TLV_IE, tag=0xA3):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.103
|
||||
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0xBA):
|
||||
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0x3A):
|
||||
_construct = FlagsEnum(Byte, even_if_navigating_menus=0, even_if_data_call=1, even_if_voice_call=2)
|
||||
|
||||
# TS 102 223 Section 8.104
|
||||
class DnsServerAddress(COMPR_TLV_IE, tag=0xC0):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.105
|
||||
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
||||
@@ -734,7 +683,7 @@ class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
||||
_construct = GreedyRange(AccessTechTuple)
|
||||
|
||||
# TS 102 223 Section 8.107
|
||||
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0xBB):
|
||||
class ApplicationSpecificRefreshData(COMPR_TLV_IE, tag=0x3B):
|
||||
pass
|
||||
|
||||
# TS 31.111 Section 8.108
|
||||
@@ -768,194 +717,19 @@ class SMSCBDownload(BER_TLV_IE, tag=0xD2,
|
||||
nested=[DeviceIdentities, CBSPage]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class MenuSelection(BER_TLV_IE, tag=0xD3,
|
||||
nested=[DeviceIdentities, ItemIdentifier, HelpRequest]):
|
||||
pass
|
||||
|
||||
class BcRepeatIndicator(BER_TLV_IE, tag=0x2A):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class CallControl(BER_TLV_IE, tag=0xD4,
|
||||
nested=[DeviceIdentities, Address, CapabilityConfigParams, Subaddress,
|
||||
LocationInformation, BcRepeatIndicator]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class MoShortMessageControl(BER_TLV_IE, tag=0xD5):
|
||||
pass
|
||||
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class TransactionIdentifier(BER_TLV_IE, tag=0x1C):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ImsURI(BER_TLV_IE, tag=0x31):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class UriTruncated(BER_TLV_IE, tag=0x73):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class TrackingAreaIdentification(BER_TLV_IE, tag=0x7D):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ExtendedRejectionCauseCode(BER_TLV_IE, tag=0x57):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class CsgCellSelectionStatus(BER_TLV_IE, tag=0x55):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class CsgId(BER_TLV_IE, tag=0x56):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class HnbName(BER_TLV_IE, tag=0x57):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class PlmnId(BER_TLV_IE, tag=0x09):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ImsCallDisconnectionStatus(BER_TLV_IE, tag=0x55):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class Iari(BER_TLV_IE, tag=0x76):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ImpuList(BER_TLV_IE, tag=0x77):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class ImsStatusCode(BER_TLV_IE, tag=0x77):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class DateTimeAndTimezone(BER_TLV_IE, tag=0x26):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class PdpPdnPduType(BER_TLV_IE, tag=0x0B):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class GadShape(BER_TLV_IE, tag=0x77):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class NmeaSentence(BER_TLV_IE, tag=0x78):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.23
|
||||
class WlanAccessStatus(BER_TLV_IE, tag=0x4B):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class EventDownload(BER_TLV_IE, tag=0xD6,
|
||||
nested=[EventList, DeviceIdentities,
|
||||
# 7.5.1.2 (I-)WLAN Access Status
|
||||
WlanAccessStatus,
|
||||
# 7.5.1A.2 MT Call
|
||||
TransactionIdentifier, Address,
|
||||
Subaddress, ImsURI, MediaType, UriTruncated,
|
||||
# 7.5.2.2 Network Rejection
|
||||
LocationInformation, RoutingAreaIdentification, TrackingAreaIdentification,
|
||||
AccessTechnology, UpdateAttachRegistrationType, RejectionCauseCode,
|
||||
ExtendedRejectionCauseCode,
|
||||
# 7.5.2A.2 Call Connected
|
||||
# TransactionIdentifier, MediaType
|
||||
# 7.5.3.2 CSG Cell Selection
|
||||
# AccessTechnology
|
||||
CsgCellSelectionStatus, CsgId, HnbName, PlmnId,
|
||||
# 7.5.3A.2 CAll Disconnected
|
||||
# TransactionIdentifier, MediaType,
|
||||
ImsCallDisconnectionStatus,
|
||||
# TS 102 223 7.5.4 LocationStatusEvent
|
||||
# TS 102 223 7.5.5 UserActivityEvent
|
||||
# TS 102 223 7.5.6 IdleScreenAvailableEvent
|
||||
# TS 102 223 7.5.7 CardReaderStatusEvent
|
||||
# TS 102 223 7.5.8 LanguageSelectionEvent
|
||||
# TS 102 223 7.5.9 BrowserTerminationEvent
|
||||
# TS 102 223 7.5.10 DataAvailableEvent
|
||||
ChannelStatus, ChannelDataLength,
|
||||
# TS 102 223 7.5.11 ChannelStatusEvent
|
||||
# TS 102 223 7.5.12 AccessTechnologyChangeEvent
|
||||
# TS 102 223 7.5.13 DisplayParametersChangedEvent
|
||||
# TS 102 223 7.5.14 LocalConnectionEvent
|
||||
# TS 102 223 7.5.15 NetworkSearchModeChangeEvent
|
||||
# TS 102 223 7.5.16 BrowsingStatusEvent
|
||||
# TS 102 223 7.5.17 FramesInformationChangedEvent
|
||||
# 7.5.20 Incoming IMS Data
|
||||
Iari,
|
||||
# 7.5.21 MS Registration Event
|
||||
ImpuList, ImsStatusCode,
|
||||
# 7.5.24 / TS 102 223 7.5.22 PollIntervalNegotiation
|
||||
# 7.5.25 DataConnectionStatusChangeEvent
|
||||
DataConnectionStatus, DataConnectionType, SmCause,
|
||||
# TransactionIdentifier, LocationInformation, AccessTechnology
|
||||
DateTimeAndTimezone, LocationStatus, NetworkAccessName, PdpPdnPduType,
|
||||
# 7.7 / TS 102 223 7.6 MMS Transfer Status
|
||||
# 7.8 / TS 102 223 MMS Notification Download
|
||||
# 7.9 / TS 102 223 8.8 Terminal Applications
|
||||
]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class TimerExpiration(BER_TLV_IE, tag=0xD7):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + TS 31.111 7.6.2
|
||||
class USSDDownload(BER_TLV_IE, tag=0xD9,
|
||||
nested=[DeviceIdentities, USSDString]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + TS 102 223 7.6
|
||||
class MmsTransferStatus(BER_TLV_IE, tag=0xDA):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + 102 223
|
||||
class MmsNotificationDownload(BER_TLV_IE, tag=0xDB):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + 102 223 7.8
|
||||
class TerminalApplication(BER_TLV_IE, tag=0xDC):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + TS 31.111 7.10.2
|
||||
class GeographicalLocation(BER_TLV_IE, tag=0xDD,
|
||||
nested=[DeviceIdentities, GadShape, NmeaSentence]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class EnvelopeContainer(BER_TLV_IE, tag=0xDE):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class ProSeReport(BER_TLV_IE, tag=0xDF):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17
|
||||
class ProactiveCmd(BER_TLV_IE):
|
||||
def _compute_tag(self) -> int:
|
||||
return 0xD0
|
||||
|
||||
|
||||
class EventCollection(TLV_IE_Collection,
|
||||
nested=[SMSPPDownload, SMSCBDownload,
|
||||
EventDownload, CallControl, MoShortMessageControl,
|
||||
USSDDownload, GeographicalLocation, ProSeReport]):
|
||||
pass
|
||||
|
||||
|
||||
# TS 101 220 Table 7.17 + 102 223 6.6.13/9.4 + TS 31.111 6.6.13
|
||||
class Refresh(ProactiveCmd, tag=0x01,
|
||||
nested=[CommandDetails, DeviceIdentities, FileList, Aid, AlphaIdentifier,
|
||||
@@ -963,24 +737,20 @@ class Refresh(ProactiveCmd, tag=0x01,
|
||||
ApplicationSpecificRefreshData, PlmnWactList, PlmnList]):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 6.6.4
|
||||
class MoreTime(ProactiveCmd, tag=0x02,
|
||||
nested=[CommandDetails, DeviceIdentities]):
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 6.6.5
|
||||
class PollInterval(ProactiveCmd, tag=0x03,
|
||||
nested=[CommandDetails, DeviceIdentities, Duration]):
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 6.6.14
|
||||
class PollingOff(ProactiveCmd, tag=0x04,
|
||||
nested=[CommandDetails, DeviceIdentities]):
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
# TS 102 223 Section 6.6.16
|
||||
class SetUpEventList(ProactiveCmd, tag=0x05,
|
||||
nested=[CommandDetails, DeviceIdentities, EventList]):
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
# TS 31.111 Section 6.6.12
|
||||
@@ -1008,27 +778,20 @@ class SendShortMessage(ProactiveCmd, tag=0x13,
|
||||
SMS_TPDU, IconIdentifier, TextAttribute, FrameIdentifier]):
|
||||
pass
|
||||
|
||||
# TS 102 223 6.6.24
|
||||
class SendDTMF(ProactiveCmd, tag=0x14,
|
||||
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
|
||||
DtmfString, IconIdentifier, TextAttribute, FrameIdentifier]):
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
# TS 102 223 6.6.26
|
||||
class LaunchBrowser(ProactiveCmd, tag=0x15,
|
||||
nested=[CommandDetails, DeviceIdentities, BrowserIdentity, Url, Bearer, ProvisioningFileReference,
|
||||
TextString, AlphaIdentifier, IconIdentifier, TextAttribute, FrameIdentifier,
|
||||
NetworkAccessName]):
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
class GeographicalLocationRequest(ProactiveCmd, tag=0x16,
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
# TS 102 223 6.6.5
|
||||
class PlayTone(ProactiveCmd, tag=0x20,
|
||||
nested=[CommandDetails, DeviceIdentities, AlphaIdentifier,
|
||||
Tone, Duration, IconIdentifier, TextAttribute, FrameIdentifier]):
|
||||
nested=[CommandDetails]):
|
||||
pass
|
||||
|
||||
# TS 101 220 Table 7.17 + 102 223 6.6.1/9.4 CMD=0x21
|
||||
@@ -1215,8 +978,8 @@ class ProactiveCommandBase(BER_TLV_IE, tag=0xD0, nested=[CommandDetails]):
|
||||
for c in self.children:
|
||||
if type(c).__name__ == 'CommandDetails':
|
||||
return c
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
class ProactiveCommand(TLV_IE_Collection,
|
||||
nested=[Refresh, MoreTime, PollInterval, PollingOff, SetUpEventList, SetUpCall,
|
||||
@@ -1234,17 +997,17 @@ class ProactiveCommand(TLV_IE_Collection,
|
||||
more difficult than any normal TLV IE Collection, because the content of one of the IEs defines the
|
||||
definitions of all the other IEs. So we first need to find the CommandDetails, and then parse according
|
||||
to the command type indicated in that IE data."""
|
||||
def from_bytes(self, binary: bytes, context: dict = {}) -> List[TLV_IE]:
|
||||
def from_bytes(self, binary: bytes) -> List[TLV_IE]:
|
||||
# do a first parse step to get the CommandDetails
|
||||
pcmd = ProactiveCommandBase()
|
||||
pcmd.from_tlv(binary)
|
||||
cmd_details = pcmd.find_cmd_details()
|
||||
# then do a second decode stage for the specific
|
||||
cmd_type = TypeOfCommand.encmapping[cmd_details.decoded['type_of_command']]
|
||||
cmd_type = cmd_details.decoded['type_of_command']
|
||||
if cmd_type in self.members_by_tag:
|
||||
cls = self.members_by_tag[cmd_type]
|
||||
inst = cls()
|
||||
_dec, remainder = inst.from_tlv(binary)
|
||||
dec, remainder = inst.from_tlv(binary)
|
||||
self.decoded = inst
|
||||
else:
|
||||
self.decoded = pcmd
|
||||
@@ -1256,18 +1019,9 @@ class ProactiveCommand(TLV_IE_Collection,
|
||||
def to_dict(self):
|
||||
return self.decoded.to_dict()
|
||||
|
||||
def to_bytes(self, context: dict = {}):
|
||||
def to_bytes(self):
|
||||
return self.decoded.to_tlv()
|
||||
|
||||
# TS 101 223 Section 6.8.0
|
||||
class TerminalResponse(TLV_IE_Collection,
|
||||
nested=[CommandDetails, DeviceIdentities, Result,
|
||||
Duration, TextString, ItemIdentifier,
|
||||
#TODO: LocalInformation and other optional/conditional IEs
|
||||
ChannelData, ChannelDataLength,
|
||||
ChannelStatus, BufferSize, BearerDescription,
|
||||
]):
|
||||
pass
|
||||
|
||||
# reasonable default for playing with OTA
|
||||
# 010203040506070809101112131415161718192021222324252627282930313233
|
||||
|
||||
@@ -19,15 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import enum
|
||||
|
||||
from construct import Bytewise, BitStruct, BitsInteger, Struct, FlagsEnum
|
||||
from osmocom.utils import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import match_ruim
|
||||
from pySim.profile import CardProfile, CardProfileAddon
|
||||
from pySim.ts_51_011 import CardProfileSIM
|
||||
from pySim.ts_51_011 import DF_TELECOM, DF_GSM
|
||||
from pySim.ts_51_011 import EF_ServiceTable
|
||||
from pySim.construct import *
|
||||
from construct import *
|
||||
|
||||
|
||||
# Mapping between CDMA Service Number and its description
|
||||
@@ -115,7 +115,7 @@ class EF_AD(TransparentEF):
|
||||
'''3.4.33 Administrative Data'''
|
||||
|
||||
_test_de_encode = [
|
||||
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : b'\x00\x00', 'rfu' : b'' } ),
|
||||
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : '' } ),
|
||||
]
|
||||
_test_no_pad = True
|
||||
|
||||
@@ -134,9 +134,9 @@ class EF_AD(TransparentEF):
|
||||
# Byte 1: Display Condition
|
||||
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
|
||||
# Bytes 2-3: Additional information
|
||||
'additional_info'/Bytes(2),
|
||||
'additional_info'/HexAdapter(Bytes(2)),
|
||||
# Bytes 4..: RFU
|
||||
'rfu'/GreedyBytesRFU,
|
||||
'rfu'/HexAdapter(GreedyBytesRFU),
|
||||
)
|
||||
|
||||
|
||||
@@ -178,23 +178,20 @@ class DF_CDMA(CardDF):
|
||||
class CardProfileRUIM(CardProfile):
|
||||
'''R-UIM card profile as per 3GPP2 C.S0023-D'''
|
||||
|
||||
ORDER = 20
|
||||
ORDER = 2
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('R-UIM', desc='CDMA R-UIM Card', cla="a0",
|
||||
sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
|
||||
|
||||
@staticmethod
|
||||
def decode_select_response(data_hex: str) -> object:
|
||||
def decode_select_response(resp_hex: str) -> object:
|
||||
# TODO: Response parameters/data in case of DF_CDMA (section 2.6)
|
||||
return CardProfileSIM.decode_select_response(data_hex)
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
""" Try to access MF/DF.CDMA via 2G APDUs (3GPP TS 11.11), if this works,
|
||||
the card is considered an R-UIM card for CDMA."""
|
||||
cls._mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
|
||||
return CardProfileSIM.decode_select_response(resp_hex)
|
||||
|
||||
@staticmethod
|
||||
def match_with_card(scc: SimCardCommands) -> bool:
|
||||
return match_ruim(scc)
|
||||
|
||||
class AddonRUIM(CardProfileAddon):
|
||||
"""An Addon that can be found on on a combined SIM + RUIM or UICC + RUIM to support CDMA."""
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#
|
||||
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
|
||||
# Copyright (C) 2010-2024 Harald Welte <laforge@gnumonks.org>
|
||||
# Copyright (C) 2010-2023 Harald Welte <laforge@gnumonks.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -21,15 +21,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from typing import List, Tuple
|
||||
from typing import List, Optional, Tuple
|
||||
import typing # construct also has a Union, so we do typing.Union below
|
||||
from construct import Construct, Struct, Const, Select
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import LV, filter_dict
|
||||
from osmocom.utils import rpad, lpad, b2h, h2b, h2i, i2h, str_sanitize, Hexstr
|
||||
from osmocom.tlv import bertlv_encode_len
|
||||
|
||||
from pySim.utils import sw_match, expand_hex, SwHexstr, ResTuple, SwMatchstr
|
||||
from construct import *
|
||||
from pySim.construct import LV
|
||||
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.transport import LinkBase
|
||||
|
||||
@@ -66,115 +64,41 @@ class SimCardCommands:
|
||||
byte by the respective instance. """
|
||||
def __init__(self, transport: LinkBase, lchan_nr: int = 0):
|
||||
self._tp = transport
|
||||
self._cla_byte = None
|
||||
self.sel_ctrl = "0000"
|
||||
self.lchan_nr = lchan_nr
|
||||
# invokes the setter below
|
||||
self.cla_byte = "a0"
|
||||
self.scp = None # Secure Channel Protocol
|
||||
|
||||
def fork_lchan(self, lchan_nr: int) -> 'SimCardCommands':
|
||||
"""Fork a per-lchan specific SimCardCommands instance off the current instance."""
|
||||
ret = SimCardCommands(transport = self._tp, lchan_nr = lchan_nr)
|
||||
ret.cla_byte = self.cla_byte
|
||||
ret.cla_byte = self._cla_byte
|
||||
ret.sel_ctrl = self.sel_ctrl
|
||||
return ret
|
||||
|
||||
@property
|
||||
def max_cmd_len(self) -> int:
|
||||
"""Maximum length of the command apdu data section. Depends on secure channel protocol used."""
|
||||
if self.scp:
|
||||
return 255 - self.scp.overhead
|
||||
def cla_byte(self) -> Hexstr:
|
||||
"""Return the (cached) patched default CLA byte for this card."""
|
||||
return self._cla4lchan
|
||||
|
||||
@cla_byte.setter
|
||||
def cla_byte(self, new_val: Hexstr):
|
||||
"""Set the (raw, without lchan) default CLA value for this card."""
|
||||
self._cla_byte = new_val
|
||||
# compute cached result
|
||||
self._cla4lchan = cla_with_lchan(self._cla_byte, self.lchan_nr)
|
||||
|
||||
def cla4lchan(self, cla: Hexstr) -> Hexstr:
|
||||
"""Compute the lchan-patched value of the given CLA value. If no CLA
|
||||
value is provided as argument, the lchan-patched version of the SimCardCommands._cla_byte
|
||||
value is used. Most commands will use the latter, while some wish to override it and
|
||||
can pass it as argument here."""
|
||||
if not cla:
|
||||
# return cached result to avoid re-computing this over and over again
|
||||
return self._cla4lchan
|
||||
else:
|
||||
return 255
|
||||
|
||||
def send_apdu(self, pdu: Hexstr, apply_lchan:bool = True) -> ResTuple:
|
||||
"""Sends an APDU and auto fetch response data
|
||||
|
||||
Args:
|
||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
||||
Returns:
|
||||
tuple(data, sw), where
|
||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||
sw : string (in hex) of status word (ex. "9000")
|
||||
"""
|
||||
if apply_lchan:
|
||||
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
|
||||
if self.scp:
|
||||
return self.scp.send_apdu_wrapper(self._tp.send_apdu, pdu)
|
||||
else:
|
||||
return self._tp.send_apdu(pdu)
|
||||
|
||||
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000", apply_lchan:bool = True) -> ResTuple:
|
||||
"""Sends an APDU and check returned SW
|
||||
|
||||
Args:
|
||||
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
|
||||
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
|
||||
digits using a '?' to add some ambiguity if needed.
|
||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
||||
Returns:
|
||||
tuple(data, sw), where
|
||||
data : string (in hex) of returned data (ex. "074F4EFFFF")
|
||||
sw : string (in hex) of status word (ex. "9000")
|
||||
"""
|
||||
if apply_lchan:
|
||||
pdu = cla_with_lchan(pdu[0:2], self.lchan_nr) + pdu[2:]
|
||||
if self.scp:
|
||||
return self.scp.send_apdu_wrapper(self._tp.send_apdu_checksw, pdu, sw)
|
||||
else:
|
||||
return self._tp.send_apdu_checksw(pdu, sw)
|
||||
|
||||
def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr, cmd_constr: Construct,
|
||||
cmd_data: Hexstr, resp_constr: Construct, apply_lchan:bool = True) -> Tuple[dict, SwHexstr]:
|
||||
"""Build and sends an APDU using a 'construct' definition; parses response.
|
||||
|
||||
Args:
|
||||
cla : string (in hex) ISO 7816 class byte
|
||||
ins : string (in hex) ISO 7816 instruction byte
|
||||
p1 : string (in hex) ISO 7116 Parameter 1 byte
|
||||
p2 : string (in hex) ISO 7116 Parameter 2 byte
|
||||
cmd_cosntr : defining how to generate binary APDU command data
|
||||
cmd_data : command data passed to cmd_constr
|
||||
resp_cosntr : defining how to decode binary APDU response data
|
||||
apply_lchan : apply the currently selected lchan to the CLA byte before sending
|
||||
Returns:
|
||||
Tuple of (decoded_data, sw)
|
||||
"""
|
||||
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
|
||||
lc = i2h([len(cmd)]) if cmd_data else ''
|
||||
le = '00' if resp_constr else ''
|
||||
pdu = ''.join([cla, ins, p1, p2, lc, b2h(cmd), le])
|
||||
(data, sw) = self.send_apdu(pdu, apply_lchan = apply_lchan)
|
||||
if data:
|
||||
# filter the resulting dict to avoid '_io' members inside
|
||||
rsp = filter_dict(resp_constr.parse(h2b(data)))
|
||||
else:
|
||||
rsp = None
|
||||
return (rsp, sw)
|
||||
|
||||
def send_apdu_constr_checksw(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr,
|
||||
cmd_constr: Construct, cmd_data: Hexstr, resp_constr: Construct,
|
||||
sw_exp: SwMatchstr="9000", apply_lchan:bool = True) -> Tuple[dict, SwHexstr]:
|
||||
"""Build and sends an APDU using a 'construct' definition; parses response.
|
||||
|
||||
Args:
|
||||
cla : string (in hex) ISO 7816 class byte
|
||||
ins : string (in hex) ISO 7816 instruction byte
|
||||
p1 : string (in hex) ISO 7116 Parameter 1 byte
|
||||
p2 : string (in hex) ISO 7116 Parameter 2 byte
|
||||
cmd_cosntr : defining how to generate binary APDU command data
|
||||
cmd_data : command data passed to cmd_constr
|
||||
resp_cosntr : defining how to decode binary APDU response data
|
||||
exp_sw : string (in hex) of status word (ex. "9000")
|
||||
Returns:
|
||||
Tuple of (decoded_data, sw)
|
||||
"""
|
||||
(rsp, sw) = self.send_apdu_constr(cla, ins, p1, p2, cmd_constr, cmd_data, resp_constr,
|
||||
apply_lchan = apply_lchan)
|
||||
if not sw_match(sw, sw_exp):
|
||||
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
|
||||
return (rsp, sw)
|
||||
return cla_with_lchan(cla, self.lchan_nr)
|
||||
|
||||
# Extract a single FCP item from TLV
|
||||
def __parse_fcp(self, fcp: Hexstr):
|
||||
@@ -196,7 +120,6 @@ class SimCardCommands:
|
||||
# checking if the length of the remaining TLV string matches
|
||||
# what we get in the length field.
|
||||
# See also ETSI TS 102 221, chapter 11.1.1.3.0 Base coding.
|
||||
# TODO: this likely just is normal BER-TLV ("All data objects are BER-TLV except if otherwise # defined.")
|
||||
exp_tlv_len = int(fcp[2:4], 16)
|
||||
if len(fcp[4:]) // 2 == exp_tlv_len:
|
||||
skip = 4
|
||||
@@ -204,7 +127,6 @@ class SimCardCommands:
|
||||
exp_tlv_len = int(fcp[2:6], 16)
|
||||
if len(fcp[4:]) // 2 == exp_tlv_len:
|
||||
skip = 6
|
||||
raise ValueError('Cannot determine length of TLV-length')
|
||||
|
||||
# Skip FCP tag and length
|
||||
tlv = fcp[skip:]
|
||||
@@ -245,10 +167,11 @@ class SimCardCommands:
|
||||
"""
|
||||
|
||||
rv = []
|
||||
if not isinstance(dir_list, list):
|
||||
if type(dir_list) is not list:
|
||||
dir_list = [dir_list]
|
||||
for i in dir_list:
|
||||
data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i + "00")
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
|
||||
rv.append((data, sw))
|
||||
if sw != '9000':
|
||||
return rv
|
||||
@@ -264,10 +187,10 @@ class SimCardCommands:
|
||||
list of return values (FCP in hex encoding) for each element of the path
|
||||
"""
|
||||
rv = []
|
||||
if not isinstance(dir_list, list):
|
||||
if type(dir_list) is not list:
|
||||
dir_list = [dir_list]
|
||||
for i in dir_list:
|
||||
data, _sw = self.select_file(i)
|
||||
data, sw = self.select_file(i)
|
||||
rv.append(data)
|
||||
return rv
|
||||
|
||||
@@ -278,11 +201,11 @@ class SimCardCommands:
|
||||
fid : file identifier as hex string
|
||||
"""
|
||||
|
||||
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid + "00")
|
||||
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
|
||||
|
||||
def select_parent_df(self) -> ResTuple:
|
||||
"""Execute SELECT to switch to the parent DF """
|
||||
return self.send_apdu_checksw(self.cla_byte + "a40304")
|
||||
return self._tp.send_apdu_checksw(self.cla_byte + "a4030400")
|
||||
|
||||
def select_adf(self, aid: Hexstr) -> ResTuple:
|
||||
"""Execute SELECT a given Applicaiton ADF.
|
||||
@@ -292,7 +215,7 @@ class SimCardCommands:
|
||||
"""
|
||||
|
||||
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
|
||||
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid + "00")
|
||||
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
|
||||
|
||||
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
|
||||
"""Execute READD BINARY.
|
||||
@@ -313,14 +236,14 @@ class SimCardCommands:
|
||||
total_data = ''
|
||||
chunk_offset = 0
|
||||
while chunk_offset < length:
|
||||
chunk_len = min(self.max_cmd_len, length-chunk_offset)
|
||||
chunk_len = min(255, length-chunk_offset)
|
||||
pdu = self.cla_byte + \
|
||||
'b0%04x%02x' % (offset + chunk_offset, chunk_len)
|
||||
try:
|
||||
data, sw = self.send_apdu_checksw(pdu)
|
||||
data, sw = self._tp.send_apdu_checksw(pdu)
|
||||
except Exception as e:
|
||||
e.add_note('failed to read (offset %d)' % offset)
|
||||
raise e
|
||||
raise ValueError('%s, failed to read (offset %d)' %
|
||||
(str_sanitize(str(e)), offset))
|
||||
total_data += data
|
||||
chunk_offset += chunk_len
|
||||
return total_data, sw
|
||||
@@ -371,16 +294,16 @@ class SimCardCommands:
|
||||
total_data = ''
|
||||
chunk_offset = 0
|
||||
while chunk_offset < data_length:
|
||||
chunk_len = min(self.max_cmd_len, data_length - chunk_offset)
|
||||
chunk_len = min(255, data_length - chunk_offset)
|
||||
# chunk_offset is bytes, but data slicing is hex chars, so we need to multiply by 2
|
||||
pdu = self.cla_byte + \
|
||||
'd6%04x%02x' % (offset + chunk_offset, chunk_len) + \
|
||||
data[chunk_offset*2: (chunk_offset+chunk_len)*2]
|
||||
try:
|
||||
chunk_data, chunk_sw = self.send_apdu_checksw(pdu)
|
||||
chunk_data, chunk_sw = self._tp.send_apdu_checksw(pdu)
|
||||
except Exception as e:
|
||||
e.add_note('failed to write chunk (chunk_offset %d, chunk_len %d)' % (chunk_offset, chunk_len))
|
||||
raise e
|
||||
raise ValueError('%s, failed to write chunk (chunk_offset %d, chunk_len %d)' %
|
||||
(str_sanitize(str(e)), chunk_offset, chunk_len))
|
||||
total_data += data
|
||||
chunk_offset += chunk_len
|
||||
if verify:
|
||||
@@ -397,7 +320,7 @@ class SimCardCommands:
|
||||
r = self.select_path(ef)
|
||||
rec_length = self.__record_len(r)
|
||||
pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length)
|
||||
return self.send_apdu_checksw(pdu)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
def __verify_record(self, ef: Path, rec_no: int, data: str):
|
||||
"""Verify record against given data
|
||||
@@ -436,10 +359,10 @@ class SimCardCommands:
|
||||
else:
|
||||
# make sure the input data is padded to the record length using 0xFF.
|
||||
# In cases where the input data exceed we throw an exception.
|
||||
if len(data) // 2 > rec_length:
|
||||
if (len(data) // 2 > rec_length):
|
||||
raise ValueError('Data length exceeds record length (expected max %d, got %d)' % (
|
||||
rec_length, len(data) // 2))
|
||||
elif len(data) // 2 < rec_length:
|
||||
elif (len(data) // 2 < rec_length):
|
||||
if leftpad:
|
||||
data = lpad(data, rec_length * 2)
|
||||
else:
|
||||
@@ -460,7 +383,7 @@ class SimCardCommands:
|
||||
pass
|
||||
|
||||
pdu = (self.cla_byte + 'dc%02x04%02x' % (rec_no, rec_length)) + data
|
||||
res = self.send_apdu_checksw(pdu)
|
||||
res = self._tp.send_apdu_checksw(pdu)
|
||||
if verify:
|
||||
self.__verify_record(ef, rec_no, data)
|
||||
return res
|
||||
@@ -495,10 +418,10 @@ class SimCardCommands:
|
||||
# TS 102 221 Section 11.3.1 low-level helper
|
||||
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
|
||||
if first:
|
||||
pdu = '80cb008001%02x00' % (tag)
|
||||
pdu = self.cla4lchan('80') + 'cb008001%02x' % (tag)
|
||||
else:
|
||||
pdu = '80cb0000'
|
||||
return self.send_apdu_checksw(pdu)
|
||||
pdu = self.cla4lchan('80') + 'cb000000'
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
|
||||
"""Execute RETRIEVE DATA, see also TS 102 221 Section 11.3.1.
|
||||
@@ -514,7 +437,7 @@ class SimCardCommands:
|
||||
# retrieve first block
|
||||
data, sw = self._retrieve_data(tag, first=True)
|
||||
total_data += data
|
||||
while sw in ['62f1', '62f2']:
|
||||
while sw == '62f1' or sw == '62f2':
|
||||
data, sw = self._retrieve_data(tag, first=False)
|
||||
total_data += data
|
||||
return total_data, sw
|
||||
@@ -525,10 +448,10 @@ class SimCardCommands:
|
||||
p1 = 0x80
|
||||
else:
|
||||
p1 = 0x00
|
||||
if isinstance(data, (bytes, bytearray)):
|
||||
if isinstance(data, bytes) or isinstance(data, bytearray):
|
||||
data = b2h(data)
|
||||
pdu = '80db00%02x%02x%s' % (p1, len(data)//2, data)
|
||||
return self.send_apdu_checksw(pdu)
|
||||
pdu = self.cla4lchan('80') + 'db00%02x%02x%s' % (p1, len(data)//2, data)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
def set_data(self, ef, tag: int, value: str, verify: bool = False, conserve: bool = False) -> ResTuple:
|
||||
"""Execute SET DATA.
|
||||
@@ -555,10 +478,10 @@ class SimCardCommands:
|
||||
total_len = len(tlv_bin)
|
||||
remaining = tlv_bin
|
||||
while len(remaining) > 0:
|
||||
fragment = remaining[:self.max_cmd_len]
|
||||
fragment = remaining[:255]
|
||||
rdata, sw = self._set_data(fragment, first=first)
|
||||
first = False
|
||||
remaining = remaining[self.max_cmd_len:]
|
||||
remaining = remaining[255:]
|
||||
return rdata, sw
|
||||
|
||||
def run_gsm(self, rand: Hexstr) -> ResTuple:
|
||||
@@ -570,7 +493,7 @@ class SimCardCommands:
|
||||
if len(rand) != 32:
|
||||
raise ValueError('Invalid rand')
|
||||
self.select_path(['3f00', '7f20'])
|
||||
return self.send_apdu_checksw('a088000010' + rand + '00', sw='9000')
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('a0') + '88000010' + rand, sw='9000')
|
||||
|
||||
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
|
||||
"""Execute AUTHENTICATE (USIM/ISIM).
|
||||
@@ -581,9 +504,10 @@ class SimCardCommands:
|
||||
context : 16 byte random data ('3g' or 'gsm')
|
||||
"""
|
||||
# 3GPP TS 31.102 Section 7.1.2.1
|
||||
AuthCmd3G = Struct('rand'/LV, 'autn'/COptional(LV))
|
||||
AuthCmd3G = Struct('rand'/LV, 'autn'/Optional(LV))
|
||||
AuthResp3GSyncFail = Struct(Const(b'\xDC'), 'auts'/LV)
|
||||
AuthResp3GSuccess = Struct(Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/COptional(LV))
|
||||
AuthResp3GSuccess = Struct(
|
||||
Const(b'\xDB'), 'res'/LV, 'ck'/LV, 'ik'/LV, 'kc'/Optional(LV))
|
||||
AuthResp3G = Select(AuthResp3GSyncFail, AuthResp3GSuccess)
|
||||
# build parameters
|
||||
cmd_data = {'rand': rand, 'autn': autn}
|
||||
@@ -591,9 +515,7 @@ class SimCardCommands:
|
||||
p2 = '81'
|
||||
elif context == 'gsm':
|
||||
p2 = '80'
|
||||
else:
|
||||
raise ValueError("Unsupported context '%s'" % context)
|
||||
(data, sw) = self.send_apdu_constr_checksw(
|
||||
(data, sw) = self._tp.send_apdu_constr_checksw(
|
||||
self.cla_byte, '88', '00', p2, AuthCmd3G, cmd_data, AuthResp3G)
|
||||
if 'auts' in data:
|
||||
ret = {'synchronisation_failure': data}
|
||||
@@ -603,11 +525,11 @@ class SimCardCommands:
|
||||
|
||||
def status(self) -> ResTuple:
|
||||
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
|
||||
return self.send_apdu_checksw('80F20000')
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('80') + 'F20000ff')
|
||||
|
||||
def deactivate_file(self) -> ResTuple:
|
||||
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
|
||||
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:
|
||||
"""Execute ACTIVATE FILE command as per TS 102 221 Section 11.1.15.
|
||||
@@ -615,31 +537,31 @@ class SimCardCommands:
|
||||
Args:
|
||||
fid : file identifier as hex string
|
||||
"""
|
||||
return self.send_apdu_checksw(self.cla_byte + '44000002' + fid)
|
||||
return self._tp.send_apdu_checksw(self.cla_byte + '44000002' + fid)
|
||||
|
||||
def create_file(self, payload: Hexstr) -> ResTuple:
|
||||
"""Execute CREATE FILE command as per TS 102 222 Section 6.3"""
|
||||
return self.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
|
||||
"""Execute CREEATE FILE command as per TS 102 222 Section 6.3"""
|
||||
return self._tp.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload))
|
||||
|
||||
def resize_file(self, payload: Hexstr) -> ResTuple:
|
||||
"""Execute RESIZE FILE command as per TS 102 222 Section 6.10"""
|
||||
return self.send_apdu_checksw('80d40000%02x%s' % (len(payload)//2, payload))
|
||||
return self._tp.send_apdu_checksw(self.cla4lchan('80') + 'd40000%02x%s' % (len(payload)//2, payload))
|
||||
|
||||
def delete_file(self, fid: Hexstr) -> ResTuple:
|
||||
"""Execute DELETE FILE command as per TS 102 222 Section 6.4"""
|
||||
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:
|
||||
"""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:
|
||||
"""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:
|
||||
"""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:
|
||||
"""Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17.
|
||||
@@ -652,8 +574,8 @@ class SimCardCommands:
|
||||
p1 = 0x80
|
||||
else:
|
||||
p1 = 0x00
|
||||
pdu = self.cla_byte + '70%02x%02x' % (p1, lchan_nr)
|
||||
return self.send_apdu_checksw(pdu)
|
||||
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
|
||||
return self._tp.send_apdu_checksw(pdu)
|
||||
|
||||
def reset_card(self) -> Hexstr:
|
||||
"""Physically reset the card"""
|
||||
@@ -663,8 +585,8 @@ class SimCardCommands:
|
||||
if sw_match(sw, '63cx'):
|
||||
raise RuntimeError('Failed to %s chv_no 0x%02X with code 0x%s, %i tries left.' %
|
||||
(op_name, chv_no, b2h(pin_code).upper(), int(sw[3])))
|
||||
if sw != '9000':
|
||||
raise SwMatchError(sw, '9000', self._tp.sw_interpreter)
|
||||
elif (sw != '9000'):
|
||||
raise SwMatchError(sw, '9000')
|
||||
|
||||
def verify_chv(self, chv_no: int, code: Hexstr) -> ResTuple:
|
||||
"""Verify a given CHV (Card Holder Verification == PIN)
|
||||
@@ -674,7 +596,8 @@ class SimCardCommands:
|
||||
code : chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
|
||||
self._chv_process_sw('verify', chv_no, code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -687,7 +610,8 @@ class SimCardCommands:
|
||||
pin_code : new chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
|
||||
self._chv_process_sw('unblock', chv_no, pin_code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -700,7 +624,8 @@ class SimCardCommands:
|
||||
new_pin_code : new chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
|
||||
self._chv_process_sw('change', chv_no, pin_code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -713,7 +638,8 @@ class SimCardCommands:
|
||||
new_pin_code : new chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(pin_code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
|
||||
self._chv_process_sw('disable', chv_no, pin_code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -725,7 +651,8 @@ class SimCardCommands:
|
||||
pin_code : chv code as hex string
|
||||
"""
|
||||
fc = rpad(b2h(pin_code), 16)
|
||||
data, sw = self.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
|
||||
data, sw = self._tp.send_apdu(
|
||||
self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
|
||||
self._chv_process_sw('enable', chv_no, pin_code, sw)
|
||||
return (data, sw)
|
||||
|
||||
@@ -735,7 +662,7 @@ class SimCardCommands:
|
||||
Args:
|
||||
payload : payload as hex string
|
||||
"""
|
||||
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload) + "00", apply_lchan = False)
|
||||
return self._tp.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload))
|
||||
|
||||
def terminal_profile(self, payload: Hexstr) -> ResTuple:
|
||||
"""Send TERMINAL PROFILE to card
|
||||
@@ -744,7 +671,7 @@ class SimCardCommands:
|
||||
payload : payload as hex string
|
||||
"""
|
||||
data_length = len(payload) // 2
|
||||
data, sw = self.send_apdu_checksw(('80100000%02x' % data_length) + payload, apply_lchan = False)
|
||||
data, sw = self._tp.send_apdu(('80100000%02x' % data_length) + payload)
|
||||
return (data, sw)
|
||||
|
||||
# ETSI TS 102 221 11.1.22
|
||||
@@ -758,31 +685,34 @@ class SimCardCommands:
|
||||
def encode_duration(secs: int) -> Hexstr:
|
||||
if secs >= 10*24*60*60:
|
||||
return '04%02x' % (secs // (10*24*60*60))
|
||||
if secs >= 24*60*60:
|
||||
elif secs >= 24*60*60:
|
||||
return '03%02x' % (secs // (24*60*60))
|
||||
if secs >= 60*60:
|
||||
elif secs >= 60*60:
|
||||
return '02%02x' % (secs // (60*60))
|
||||
if secs >= 60:
|
||||
elif secs >= 60:
|
||||
return '01%02x' % (secs // 60)
|
||||
return '00%02x' % secs
|
||||
else:
|
||||
return '00%02x' % secs
|
||||
|
||||
def decode_duration(enc: Hexstr) -> int:
|
||||
time_unit = enc[:2]
|
||||
length = h2i(enc[2:4])[0]
|
||||
if time_unit == '04':
|
||||
return length * 10*24*60*60
|
||||
if time_unit == '03':
|
||||
elif time_unit == '03':
|
||||
return length * 24*60*60
|
||||
if time_unit == '02':
|
||||
elif time_unit == '02':
|
||||
return length * 60*60
|
||||
if time_unit == '01':
|
||||
elif time_unit == '01':
|
||||
return length * 60
|
||||
if time_unit == '00':
|
||||
elif time_unit == '00':
|
||||
return length
|
||||
raise ValueError('Time unit must be 0x00..0x04')
|
||||
else:
|
||||
raise ValueError('Time unit must be 0x00..0x04')
|
||||
min_dur_enc = encode_duration(min_len_secs)
|
||||
max_dur_enc = encode_duration(max_len_secs)
|
||||
data, sw = self.send_apdu_checksw('8076000004' + min_dur_enc + max_dur_enc, apply_lchan = False)
|
||||
data, sw = self._tp.send_apdu_checksw(
|
||||
'8076000004' + min_dur_enc + max_dur_enc)
|
||||
negotiated_duration_secs = decode_duration(data[:4])
|
||||
resume_token = data[4:]
|
||||
return (negotiated_duration_secs, resume_token, sw)
|
||||
@@ -792,15 +722,14 @@ class SimCardCommands:
|
||||
"""Send SUSPEND UICC (resume) to the card."""
|
||||
if len(h2b(token)) != 8:
|
||||
raise ValueError("Token must be 8 bytes long")
|
||||
data, sw = self.send_apdu_checksw('8076010008' + token, apply_lchan = False)
|
||||
data, sw = self._tp.send_apdu_checksw('8076010008' + token)
|
||||
return (data, sw)
|
||||
|
||||
# GPC_SPE_034 11.3
|
||||
def get_data(self, tag: int, cla: int = 0x00):
|
||||
data, sw = self.send_apdu_checksw('%02xca%04x00' % (cla, tag))
|
||||
data, sw = self._tp.send_apdu('%02xca%04x00' % (cla, tag))
|
||||
return (data, sw)
|
||||
|
||||
# TS 31.102 Section 7.5.2
|
||||
def get_identity(self, context: int) -> Tuple[Hexstr, SwHexstr]:
|
||||
data, sw = self.send_apdu_checksw('807800%02x00' % (context))
|
||||
data, sw = self._tp.send_apdu_checksw('807800%02x00' % (context))
|
||||
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
|
||||
from typing import Optional, Tuple
|
||||
from importlib import resources
|
||||
|
||||
class PMO:
|
||||
"""Convenience conversion class for ProfileManagementOperation as used in ES9+ notifications."""
|
||||
pmo4operation = {
|
||||
'install': 0x80,
|
||||
'enable': 0x40,
|
||||
'disable': 0x20,
|
||||
'delete': 0x10,
|
||||
}
|
||||
import asn1tools
|
||||
|
||||
def __init__(self, op: str):
|
||||
if not op in self.pmo4operation:
|
||||
raise ValueError('Unknown operation "%s"' % op)
|
||||
self.op = op
|
||||
|
||||
def to_int(self):
|
||||
return self.pmo4operation[self.op]
|
||||
|
||||
@staticmethod
|
||||
def _num_bits(data: int)-> int:
|
||||
for i in range(0, 8):
|
||||
if data & (1 << i):
|
||||
return 8-i
|
||||
return 0
|
||||
|
||||
def to_bitstring(self) -> Tuple[bytes, int]:
|
||||
"""return value in a format as used by asn1tools for BITSTRING."""
|
||||
val = self.to_int()
|
||||
return (bytes([val]), self._num_bits(val))
|
||||
|
||||
@classmethod
|
||||
def from_int(cls, i: int) -> 'PMO':
|
||||
"""Parse an integer representation."""
|
||||
for k, v in cls.pmo4operation.items():
|
||||
if v == i:
|
||||
return cls(k)
|
||||
raise ValueError('Unknown PMO 0x%02x' % i)
|
||||
|
||||
@classmethod
|
||||
def from_bitstring(cls, bstr: Tuple[bytes, int]) -> 'PMO':
|
||||
"""Parse a asn1tools BITSTRING representation."""
|
||||
return cls.from_int(bstr[0][0])
|
||||
|
||||
def __str__(self):
|
||||
return self.op
|
||||
|
||||
def compile_asn1_subdir(subdir_name:str, codec='der'):
|
||||
def compile_asn1_subdir(subdir_name:str):
|
||||
"""Helper function that compiles ASN.1 syntax from all files within given subdir"""
|
||||
import asn1tools
|
||||
asn_txt = ''
|
||||
__ver = sys.version_info
|
||||
if (__ver.major, __ver.minor) >= (3, 9):
|
||||
@@ -58,81 +13,4 @@ def compile_asn1_subdir(subdir_name:str, codec='der'):
|
||||
asn_txt += "\n"
|
||||
#else:
|
||||
#print(resources.read_text(__name__, 'asn1/rsp.asn'))
|
||||
return asn1tools.compile_string(asn_txt, codec=codec)
|
||||
|
||||
|
||||
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)
|
||||
return asn1tools.compile_string(asn_txt, codec='der')
|
||||
|
||||
@@ -983,7 +983,7 @@ keyAccess [22] OCTET STRING (SIZE (1)) DEFAULT '00'H,
|
||||
keyIdentifier [2] OCTET STRING (SIZE (1)),
|
||||
keyVersionNumber [3] OCTET STRING (SIZE (1)),
|
||||
keyCounterValue [5] OCTET STRING OPTIONAL,
|
||||
keyComponents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
|
||||
keyCompontents SEQUENCE (SIZE (1..MAX)) OF SEQUENCE {
|
||||
keyType [0] OCTET STRING,
|
||||
keyData [6] OCTET STRING,
|
||||
macLength[7] UInt8 DEFAULT 8
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
|
||||
where BPP is the Bound Profile Package. So the full expansion is the
|
||||
"GSMA eSIM Remote SIM Provisioning Bound Profile Packate Protection Protocol"
|
||||
|
||||
Originally (SGP.22 v2.x) this was called SCP03t, but it has since been renamed to BSP."""
|
||||
|
||||
# Early proof-of-concept implementation of
|
||||
# GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
|
||||
# where BPP is the Bound Profile Package. So the full expansion is the
|
||||
# "GSMA eSIM Remote SIM Provisioning Bound Profile Packate Protection Protocol"
|
||||
#
|
||||
# Originally (SGP.22 v2.x) this was called SCP03t, but it has since been
|
||||
# renamed to BSP.
|
||||
#
|
||||
# (C) 2023 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -21,6 +23,7 @@ Originally (SGP.22 v2.x) this was called SCP03t, but it has since been renamed t
|
||||
|
||||
# SGP.22 v3.0 Section 2.5.3:
|
||||
# That block of data is split into segments of a maximum size of 1020 bytes (including the tag, length field and MAC).
|
||||
MAX_SEGMENT_SIZE = 1020
|
||||
|
||||
import abc
|
||||
from typing import List
|
||||
@@ -33,17 +36,13 @@ from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Hash import CMAC
|
||||
|
||||
from osmocom.utils import b2h
|
||||
from osmocom.tlv import bertlv_encode_len, bertlv_parse_one
|
||||
from pySim.utils import bertlv_encode_len, bertlv_parse_one, b2h
|
||||
|
||||
# don't log by default
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(logging.NullHandler())
|
||||
|
||||
MAX_SEGMENT_SIZE = 1020
|
||||
|
||||
class BspAlgo(abc.ABC):
|
||||
"""Base class representing a cryptographic algorithm within the BSP (BPP Security Protocol)."""
|
||||
blocksize: int
|
||||
|
||||
def _get_padding(self, in_len: int, multiple: int, padding: int = 0) -> bytes:
|
||||
@@ -51,17 +50,16 @@ class BspAlgo(abc.ABC):
|
||||
if in_len % multiple == 0:
|
||||
return b''
|
||||
pad_cnt = multiple - (in_len % multiple)
|
||||
return bytes([padding]) * pad_cnt
|
||||
return b'\x00' * pad_cnt
|
||||
|
||||
def _pad_to_multiple(self, indat: bytes, multiple: int, padding: int = 0) -> bytes:
|
||||
"""Pad the input data to multiples of 'multiple'."""
|
||||
return indat + self._get_padding(len(indat), multiple, padding)
|
||||
return indat + self._get_padding(len(indat), self.blocksize, padding)
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
class BspAlgoCrypt(BspAlgo, abc.ABC):
|
||||
"""Base class representing an encryption/decryption algorithm within the BSP (BPP Security Protocol)."""
|
||||
|
||||
def __init__(self, s_enc: bytes):
|
||||
self.s_enc = s_enc
|
||||
@@ -83,17 +81,19 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def _unpad(self, padded: bytes) -> bytes:
|
||||
"""Remove the padding from padded data."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _encrypt(self, data:bytes) -> bytes:
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _decrypt(self, data:bytes) -> bytes:
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
pass
|
||||
|
||||
class BspAlgoCryptAES128(BspAlgoCrypt):
|
||||
"""AES-CBC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
|
||||
name = 'AES-CBC-128'
|
||||
blocksize = 16
|
||||
|
||||
@@ -134,7 +134,6 @@ class BspAlgoCryptAES128(BspAlgoCrypt):
|
||||
|
||||
|
||||
class BspAlgoMac(BspAlgo, abc.ABC):
|
||||
"""Base class representing a message authentication code algorithm within the BSP (BPP Security Protocol)."""
|
||||
l_mac = 0 # must be overridden by derived class
|
||||
|
||||
def __init__(self, s_mac: bytes, initial_mac_chaining_value: bytes):
|
||||
@@ -167,9 +166,9 @@ class BspAlgoMac(BspAlgo, abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def _auth(self, temp_data: bytes) -> bytes:
|
||||
"""To be implemented by algorithm specific derived class."""
|
||||
pass
|
||||
|
||||
class BspAlgoMacAES128(BspAlgoMac):
|
||||
"""AES-CMAC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
|
||||
name = 'AES-CMAC-128'
|
||||
l_mac = 8
|
||||
|
||||
@@ -228,7 +227,7 @@ class BspInstance:
|
||||
return cls(s_enc, s_mac, initial_mcv)
|
||||
|
||||
def encrypt_and_mac_one(self, tag: int, plaintext:bytes) -> bytes:
|
||||
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertext."""
|
||||
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertex."""
|
||||
assert tag <= 255
|
||||
assert len(plaintext) <= self.max_payload_size
|
||||
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext))
|
||||
@@ -253,11 +252,11 @@ class BspInstance:
|
||||
return result
|
||||
|
||||
def mac_only_one(self, tag: int, plaintext: bytes) -> bytes:
|
||||
"""MAC a single plaintext TLV. Returns the protected ciphertext."""
|
||||
"""MAC a single plaintext TLV. Returns the protected ciphertex."""
|
||||
assert tag <= 255
|
||||
assert len(plaintext) < self.max_payload_size
|
||||
maced = self.m_algo.auth(tag, plaintext)
|
||||
# The data block counter for ICV calculation is incremented also for each segment with C-MAC only.
|
||||
# The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
|
||||
self.c_algo.block_nr += 1
|
||||
return maced
|
||||
|
||||
@@ -290,9 +289,7 @@ class BspInstance:
|
||||
|
||||
def demac_only_one(self, ciphertext: bytes) -> bytes:
|
||||
payload = self.m_algo.verify(ciphertext)
|
||||
_tdict, _l, val, _remain = bertlv_parse_one(payload)
|
||||
# The data block counter for ICV calculation is incremented also for each segment with C-MAC only.
|
||||
self.c_algo.block_nr += 1
|
||||
tdict, l, val, remain = bertlv_parse_one(payload)
|
||||
return val
|
||||
|
||||
def demac_only(self, ciphertext_list: List[bytes]) -> bytes:
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
"""GSMA eSIM RSP ES2+ interface according to SGP.22 v2.5"""
|
||||
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import requests
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from pySim.esim.http_json_api import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
class param:
|
||||
class Iccid(ApiParamString):
|
||||
"""String representation of 19 or 20 digits, where the 20th digit MAY optionally be the padding
|
||||
character F."""
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
data = str(data)
|
||||
# SGP.22 version prior to 2.2 do not require support for 19-digit ICCIDs, so let's always
|
||||
# encode it with padding F at the end.
|
||||
if len(data) == 19:
|
||||
data += 'F'
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def verify_encoded(cls, data):
|
||||
if len(data) not in [19, 20]:
|
||||
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
|
||||
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
# strip trailing padding (if it's 20 digits)
|
||||
if len(data) == 20 and data[-1] in ['F', 'f']:
|
||||
data = data[:-1]
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def verify_decoded(cls, data):
|
||||
data = str(data)
|
||||
if len(data) not in [19, 20]:
|
||||
raise ValueError('ICCID (%s) length (%u) invalid' % (data, len(data)))
|
||||
if len(data) == 19:
|
||||
decimal_part = data
|
||||
else:
|
||||
decimal_part = data[:-1]
|
||||
final_part = data[-1:]
|
||||
if final_part not in ['F', 'f'] and not final_part.isdecimal():
|
||||
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
|
||||
if not decimal_part.isdecimal():
|
||||
raise ValueError('ICCID (%s) contains non-decimal characters' % data)
|
||||
|
||||
|
||||
class Eid(ApiParamString):
|
||||
"""String of 32 decimal characters"""
|
||||
@classmethod
|
||||
def verify_encoded(cls, data):
|
||||
if len(data) != 32:
|
||||
raise ValueError('EID length invalid: "%s" (%u)' % (data, len(data)))
|
||||
|
||||
@classmethod
|
||||
def verify_decoded(cls, data):
|
||||
if not data.isdecimal():
|
||||
raise ValueError('EID (%s) contains non-decimal characters' % data)
|
||||
|
||||
class ProfileType(ApiParamString):
|
||||
pass
|
||||
|
||||
class MatchingId(ApiParamString):
|
||||
pass
|
||||
|
||||
class ConfirmationCode(ApiParamString):
|
||||
pass
|
||||
|
||||
class SmdsAddress(ApiParamFqdn):
|
||||
pass
|
||||
|
||||
class ReleaseFlag(ApiParamBoolean):
|
||||
pass
|
||||
|
||||
class FinalProfileStatusIndicator(ApiParamString):
|
||||
pass
|
||||
|
||||
class Timestamp(ApiParamString):
|
||||
"""String format as specified by W3C: YYYY-MM-DDThh:mm:ssTZD"""
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
return datetime.fromisoformat(data)
|
||||
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
return datetime.isoformat(data)
|
||||
|
||||
class NotificationPointId(ApiParamInteger):
|
||||
pass
|
||||
|
||||
class NotificationPointStatus(ApiParam):
|
||||
pass
|
||||
|
||||
class ResultData(ApiParamBase64):
|
||||
pass
|
||||
|
||||
class Es2PlusApiFunction(JsonHttpApiFunction):
|
||||
"""Base classs for representing an ES2+ API Function."""
|
||||
pass
|
||||
|
||||
# ES2+ DownloadOrder function (SGP.22 section 5.3.1)
|
||||
class DownloadOrder(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/downloadOrder'
|
||||
input_params = {
|
||||
'eid': param.Eid,
|
||||
'iccid': param.Iccid,
|
||||
'profileType': param.ProfileType
|
||||
}
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
'iccid': param.Iccid,
|
||||
}
|
||||
output_mandatory = ['header', 'iccid']
|
||||
|
||||
# ES2+ ConfirmOrder function (SGP.22 section 5.3.2)
|
||||
class ConfirmOrder(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/confirmOrder'
|
||||
input_params = {
|
||||
'iccid': param.Iccid,
|
||||
'eid': param.Eid,
|
||||
'matchingId': param.MatchingId,
|
||||
'confirmationCode': param.ConfirmationCode,
|
||||
'smdsAddress': param.SmdsAddress,
|
||||
'releaseFlag': param.ReleaseFlag,
|
||||
}
|
||||
input_mandatory = ['iccid', 'releaseFlag']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
'eid': param.Eid,
|
||||
'matchingId': param.MatchingId,
|
||||
'smdpAddress': SmdpAddress,
|
||||
}
|
||||
output_mandatory = ['header', 'matchingId']
|
||||
|
||||
# ES2+ CancelOrder function (SGP.22 section 5.3.3)
|
||||
class CancelOrder(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/cancelOrder'
|
||||
input_params = {
|
||||
'iccid': param.Iccid,
|
||||
'eid': param.Eid,
|
||||
'matchingId': param.MatchingId,
|
||||
'finalProfileStatusIndicator': param.FinalProfileStatusIndicator,
|
||||
}
|
||||
input_mandatory = ['finalProfileStatusIndicator', 'iccid']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
}
|
||||
output_mandatory = ['header']
|
||||
|
||||
# ES2+ ReleaseProfile function (SGP.22 section 5.3.4)
|
||||
class ReleaseProfile(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/releaseProfile'
|
||||
input_params = {
|
||||
'iccid': param.Iccid,
|
||||
}
|
||||
input_mandatory = ['iccid']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
}
|
||||
output_mandatory = ['header']
|
||||
|
||||
# ES2+ HandleDownloadProgress function (SGP.22 section 5.3.5)
|
||||
class HandleDownloadProgressInfo(Es2PlusApiFunction):
|
||||
path = '/gsma/rsp2/es2plus/handleDownloadProgressInfo'
|
||||
input_params = {
|
||||
'eid': param.Eid,
|
||||
'iccid': param.Iccid,
|
||||
'profileType': param.ProfileType,
|
||||
'timestamp': param.Timestamp,
|
||||
'notificationPointId': param.NotificationPointId,
|
||||
'notificationPointStatus': param.NotificationPointStatus,
|
||||
'resultData': param.ResultData,
|
||||
}
|
||||
input_mandatory = ['iccid', 'profileType', 'timestamp', 'notificationPointId', 'notificationPointStatus']
|
||||
expected_http_status = 204
|
||||
|
||||
|
||||
class Es2pApiClient:
|
||||
"""Main class representing a full ES2+ API client. Has one method for each API function."""
|
||||
def __init__(self, url_prefix:str, func_req_id:str, server_cert_verify: str = None, client_cert: str = None):
|
||||
self.func_id = 0
|
||||
self.session = requests.Session()
|
||||
if server_cert_verify:
|
||||
self.session.verify = server_cert_verify
|
||||
if client_cert:
|
||||
self.session.cert = client_cert
|
||||
|
||||
self.downloadOrder = DownloadOrder(url_prefix, func_req_id, self.session)
|
||||
self.confirmOrder = ConfirmOrder(url_prefix, func_req_id, self.session)
|
||||
self.cancelOrder = CancelOrder(url_prefix, func_req_id, self.session)
|
||||
self.releaseProfile = ReleaseProfile(url_prefix, func_req_id, self.session)
|
||||
self.handleDownloadProgressInfo = HandleDownloadProgressInfo(url_prefix, func_req_id, self.session)
|
||||
|
||||
def _gen_func_id(self) -> str:
|
||||
"""Generate the next function call id."""
|
||||
self.func_id += 1
|
||||
return 'FCI-%u-%u' % (time.time(), self.func_id)
|
||||
|
||||
|
||||
def call_downloadOrder(self, data: dict) -> dict:
|
||||
"""Perform ES2+ DownloadOrder function (SGP.22 section 5.3.1)."""
|
||||
return self.downloadOrder.call(data, self._gen_func_id())
|
||||
|
||||
def call_confirmOrder(self, data: dict) -> dict:
|
||||
"""Perform ES2+ ConfirmOrder function (SGP.22 section 5.3.2)."""
|
||||
return self.confirmOrder.call(data, self._gen_func_id())
|
||||
|
||||
def call_cancelOrder(self, data: dict) -> dict:
|
||||
"""Perform ES2+ CancelOrder function (SGP.22 section 5.3.3)."""
|
||||
return self.cancelOrder.call(data, self._gen_func_id())
|
||||
|
||||
def call_releaseProfile(self, data: dict) -> dict:
|
||||
"""Perform ES2+ CancelOrder function (SGP.22 section 5.3.4)."""
|
||||
return self.releaseProfile.call(data, self._gen_func_id())
|
||||
|
||||
def call_handleDownloadProgressInfo(self, data: dict) -> dict:
|
||||
"""Perform ES2+ HandleDownloadProgressInfo function (SGP.22 section 5.3.5)."""
|
||||
return self.handleDownloadProgressInfo.call(data, self._gen_func_id())
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+ as per SGP22 v3.0 Section 5.5"""
|
||||
|
||||
# Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+
|
||||
# as per SGP22 v3.0 Section 5.5
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -16,14 +17,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from osmocom.utils import b2h, h2b
|
||||
from osmocom.tlv import bertlv_encode_tag, bertlv_encode_len, bertlv_parse_one_rawtag
|
||||
from osmocom.tlv import bertlv_return_one_rawtlv
|
||||
from pySim.utils import b2h, h2b, bertlv_encode_tag, bertlv_encode_len
|
||||
|
||||
import pySim.esim.rsp as rsp
|
||||
from pySim.esim.bsp import BspInstance
|
||||
from pySim.esim import PMO
|
||||
|
||||
# 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
|
||||
@@ -77,40 +74,14 @@ class ProfileMetadata:
|
||||
self.iccid_bin = iccid_bin
|
||||
self.spn = spn
|
||||
self.profile_name = profile_name
|
||||
self.icon = None
|
||||
self.icon_type = None
|
||||
self.notifications = []
|
||||
|
||||
def set_icon(self, is_png: bool, icon_data: bytes):
|
||||
"""Set the icon that is part of the metadata."""
|
||||
if len(icon_data) > 1024:
|
||||
raise ValueError('Icon data must not exceed 1024 bytes')
|
||||
self.icon = icon_data
|
||||
if is_png:
|
||||
self.icon_type = 1
|
||||
else:
|
||||
self.icon_type = 0
|
||||
|
||||
def add_notification(self, event: str, address: str):
|
||||
"""Add an 'other' notification to the notification configuration of the metadata"""
|
||||
self.notifications.append((event, address))
|
||||
|
||||
def gen_store_metadata_request(self) -> bytes:
|
||||
"""Generate encoded (but unsigned) StoreMetadataRequest DO (SGP.22 5.5.3)"""
|
||||
"""Generate encoded (but unsigned) StoreMetadataReqest DO (SGP.22 5.5.3)"""
|
||||
smr = {
|
||||
'iccid': self.iccid_bin,
|
||||
'serviceProviderName': self.spn,
|
||||
'profileName': self.profile_name,
|
||||
}
|
||||
if self.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)
|
||||
|
||||
|
||||
@@ -212,79 +183,3 @@ class BoundProfilePackage(ProfilePackage):
|
||||
|
||||
# manual DER encode: wrap in outer SEQUENCE
|
||||
return bertlv_encode_tag(0xbf36) + bertlv_encode_len(len(bpp_seq)) + bpp_seq
|
||||
|
||||
def decode(self, euicc_ot, eid: str, bpp_bin: bytes):
|
||||
"""Decode a BPP into the PPP and subsequently UPP. This is what happens inside an eUICC."""
|
||||
|
||||
def split_bertlv_sequence(sequence: bytes) -> List[bytes]:
|
||||
remainder = sequence
|
||||
ret = []
|
||||
while remainder:
|
||||
_tag, _l, tlv, remainder = bertlv_return_one_rawtlv(remainder)
|
||||
ret.append(tlv)
|
||||
return ret
|
||||
|
||||
# we don't use rsp.asn1.decode('boundProfilePackage') here, as the BSP needs
|
||||
# fully encoded + MACed TLVs including their tag + length values.
|
||||
#bpp = rsp.asn1.decode('BoundProfilePackage', bpp_bin)
|
||||
|
||||
tag, _l, v, _remainder = bertlv_parse_one_rawtag(bpp_bin)
|
||||
if len(_remainder):
|
||||
raise ValueError('Excess data at end of TLV')
|
||||
if tag != 0xbf36:
|
||||
raise ValueError('Unexpected outer tag: %s' % tag)
|
||||
|
||||
# InitialiseSecureChannelRequest
|
||||
tag, _l, iscr_bin, remainder = bertlv_return_one_rawtlv(v)
|
||||
iscr = rsp.asn1.decode('InitialiseSecureChannelRequest', iscr_bin)
|
||||
|
||||
# configureIsdpRequest
|
||||
tag, _l, firstSeqOf87, remainder = bertlv_parse_one_rawtag(remainder)
|
||||
if tag != 0xa0:
|
||||
raise ValueError("Unexpected 'firstSequenceOf87' tag: %s" % tag)
|
||||
firstSeqOf87 = split_bertlv_sequence(firstSeqOf87)
|
||||
|
||||
# storeMetadataRequest
|
||||
tag, _l, seqOf88, remainder = bertlv_parse_one_rawtag(remainder)
|
||||
if tag != 0xa1:
|
||||
raise ValueError("Unexpected 'sequenceOf88' tag: %s" % tag)
|
||||
seqOf88 = split_bertlv_sequence(seqOf88)
|
||||
|
||||
tag, _l, tlv, remainder = bertlv_parse_one_rawtag(remainder)
|
||||
if tag == 0xa2:
|
||||
secondSeqOf87 = split_bertlv_sequence(tlv)
|
||||
tag2, _l, seqOf86, remainder = bertlv_parse_one_rawtag(remainder)
|
||||
if tag2 != 0xa3:
|
||||
raise ValueError("Unexpected 'sequenceOf86' tag: %s" % tag)
|
||||
seqOf86 = split_bertlv_sequence(seqOf86)
|
||||
elif tag == 0xa3:
|
||||
secondSeqOf87 = None
|
||||
seqOf86 = split_bertlv_sequence(tlv)
|
||||
else:
|
||||
raise ValueError("Unexpected 'secondSequenceOf87' tag: %s" % tag)
|
||||
|
||||
# extract smdoOtpk from initialiseSecureChannel
|
||||
smdp_otpk = iscr['smdpOtpk']
|
||||
|
||||
# Generate Session Keys using the CRT, opPK.DP.ECKA and otSK.EUICC.ECKA according to annex G
|
||||
smdp_public_key = ec.EllipticCurvePublicKey.from_encoded_point(euicc_ot.curve, smdp_otpk)
|
||||
self.shared_secret = euicc_ot.exchange(ec.ECDH(), smdp_public_key)
|
||||
|
||||
crt = iscr['controlRefTemplate']
|
||||
bsp = BspInstance.from_kdf(self.shared_secret, int.from_bytes(crt['keyType'], 'big'), int.from_bytes(crt['keyLen'], 'big'), crt['hostId'], h2b(eid))
|
||||
|
||||
self.encoded_configureISDPRequest = bsp.demac_and_decrypt(firstSeqOf87)
|
||||
self.configureISDPRequest = rsp.asn1.decode('ConfigureISDPRequest', self.encoded_configureISDPRequest)
|
||||
|
||||
self.encoded_storeMetadataRequest = bsp.demac_only(seqOf88)
|
||||
self.storeMetadataRequest = rsp.asn1.decode('StoreMetadataRequest', self.encoded_storeMetadataRequest)
|
||||
|
||||
if secondSeqOf87 != None:
|
||||
rsk_bin = bsp.demac_and_decrypt(secondSeqOf87)
|
||||
rsk = rsp.asn1.decode('ReplaceSessionKeysRequest', rsk_bin)
|
||||
# process replace_session_keys!
|
||||
bsp = BspInstance(rsk['ppkEnc'], rsk['ppkCmac'], rsk['initialMacChainingValue'])
|
||||
self.replaceSessionKeysRequest = rsk
|
||||
|
||||
self.upp = bsp.demac_and_decrypt(seqOf86)
|
||||
return self.upp
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
"""GSMA eSIM RSP ES9+ interface according ot SGP.22 v2.5"""
|
||||
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import requests
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pySim.esim.rsp as rsp
|
||||
from pySim.esim.http_json_api import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
class param:
|
||||
class RspAsn1Par(ApiParamBase64):
|
||||
"""Generalized RSP ASN.1 parameter: base64-wrapped ASN.1 DER. Derived classes must provide
|
||||
the asn1_type class variable to indicate the name of the ASN.1 type to use for encode/decode."""
|
||||
asn1_type = None # must be overridden by derived class
|
||||
|
||||
@classmethod
|
||||
def _decode(cls, data):
|
||||
data = ApiParamBase64.decode(data)
|
||||
return rsp.asn1.decode(cls.asn1_type, data)
|
||||
|
||||
@classmethod
|
||||
def _encode(cls, data):
|
||||
data = rsp.asn1.encode(cls.asn1_type, data)
|
||||
return ApiParamBase64.encode(data)
|
||||
|
||||
class EuiccInfo1(RspAsn1Par):
|
||||
asn1_type = 'EUICCInfo1'
|
||||
|
||||
class ServerSigned1(RspAsn1Par):
|
||||
asn1_type = 'ServerSigned1'
|
||||
|
||||
class PrepareDownloadResponse(RspAsn1Par):
|
||||
asn1_type = 'PrepareDownloadResponse'
|
||||
|
||||
class AuthenticateServerResponse(RspAsn1Par):
|
||||
asn1_type = 'AuthenticateServerResponse'
|
||||
|
||||
class SmdpSigned2(RspAsn1Par):
|
||||
asn1_type = 'SmdpSigned2'
|
||||
|
||||
class StoreMetadataRequest(RspAsn1Par):
|
||||
asn1_type = 'StoreMetadataRequest'
|
||||
|
||||
class PendingNotification(RspAsn1Par):
|
||||
asn1_type = 'PendingNotification'
|
||||
|
||||
class CancelSessionResponse(RspAsn1Par):
|
||||
asn1_type = 'CancelSessionResponse'
|
||||
|
||||
class TransactionId(ApiParamString):
|
||||
pass
|
||||
|
||||
class Es9PlusApiFunction(JsonHttpApiFunction):
|
||||
pass
|
||||
|
||||
# ES9+ InitiateAuthentication function (SGP.22 section 6.5.2.6)
|
||||
class InitiateAuthentication(Es9PlusApiFunction):
|
||||
path = '/gsma/rsp2/es9plus/initiateAuthentication'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'euiccChallenge': ApiParamBase64,
|
||||
'euiccInfo1': param.EuiccInfo1,
|
||||
'smdpAddress': SmdpAddress,
|
||||
}
|
||||
input_mandatory = ['euiccChallenge', 'euiccInfo1', 'smdpAddress']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
'transactionId': param.TransactionId,
|
||||
'serverSigned1': param.ServerSigned1,
|
||||
'serverSignature1': ApiParamBase64,
|
||||
'euiccCiPKIdToBeUsed': ApiParamBase64,
|
||||
'serverCertificate': ApiParamBase64,
|
||||
}
|
||||
output_mandatory = ['header', 'transactionId', 'serverSigned1', 'serverSignature1',
|
||||
'euiccCiPKIdToBeUsed', 'serverCertificate']
|
||||
|
||||
# ES9+ GetBoundProfilePackage function (SGP.22 section 6.5.2.7)
|
||||
class GetBoundProfilePackage(Es9PlusApiFunction):
|
||||
path = '/gsma/rsp2/es9plus/getBoundProfilePackage'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'transactionId': param.TransactionId,
|
||||
'prepareDownloadResponse': param.PrepareDownloadResponse,
|
||||
}
|
||||
input_mandatory = ['transactionId', 'prepareDownloadResponse']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
'transactionId': param.TransactionId,
|
||||
'boundProfilePackage': ApiParamBase64,
|
||||
}
|
||||
output_mandatory = ['header', 'transactionId', 'boundProfilePackage']
|
||||
|
||||
# ES9+ AuthenticateClient function (SGP.22 section 6.5.2.8)
|
||||
class AuthenticateClient(Es9PlusApiFunction):
|
||||
path= '/gsma/rsp2/es9plus/authenticateClient'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'transactionId': param.TransactionId,
|
||||
'authenticateServerResponse': param.AuthenticateServerResponse,
|
||||
}
|
||||
input_mandatory = ['transactionId', 'authenticateServerResponse']
|
||||
output_params = {
|
||||
'header': JsonResponseHeader,
|
||||
'transactionId': param.TransactionId,
|
||||
'profileMetadata': param.StoreMetadataRequest,
|
||||
'smdpSigned2': param.SmdpSigned2,
|
||||
'smdpSignature2': ApiParamBase64,
|
||||
'smdpCertificate': ApiParamBase64,
|
||||
}
|
||||
output_mandatory = ['header', 'transactionId', 'profileMetadata', 'smdpSigned2',
|
||||
'smdpSignature2', 'smdpCertificate']
|
||||
|
||||
# ES9+ HandleNotification function (SGP.22 section 6.5.2.9)
|
||||
class HandleNotification(Es9PlusApiFunction):
|
||||
path = '/gsma/rsp2/es9plus/handleNotification'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'pendingNotification': param.PendingNotification,
|
||||
}
|
||||
input_mandatory = ['pendingNotification']
|
||||
expected_http_status = 204
|
||||
|
||||
# ES9+ CancelSession function (SGP.22 section 6.5.2.10)
|
||||
class CancelSession(Es9PlusApiFunction):
|
||||
path = '/gsma/rsp2/es9plus/cancelSession'
|
||||
extra_http_req_headers = { 'User-Agent': 'gsma-rsp-lpad' }
|
||||
input_params = {
|
||||
'transactionId': param.TransactionId,
|
||||
'cancelSessionResponse': param.CancelSessionResponse,
|
||||
}
|
||||
input_mandatory = ['transactionId', 'cancelSessionResponse']
|
||||
|
||||
class Es9pApiClient:
|
||||
def __init__(self, url_prefix:str, server_cert_verify: str = None):
|
||||
self.session = requests.Session()
|
||||
self.session.verify = False # FIXME HACK
|
||||
if server_cert_verify:
|
||||
self.session.verify = server_cert_verify
|
||||
|
||||
self.initiateAuthentication = InitiateAuthentication(url_prefix, '', self.session)
|
||||
self.authenticateClient = AuthenticateClient(url_prefix, '', self.session)
|
||||
self.getBoundProfilePackage = GetBoundProfilePackage(url_prefix, '', self.session)
|
||||
self.handleNotification = HandleNotification(url_prefix, '', self.session)
|
||||
self.cancelSession = CancelSession(url_prefix, '', self.session)
|
||||
|
||||
def call_initiateAuthentication(self, data: dict) -> dict:
|
||||
return self.initiateAuthentication.call(data)
|
||||
|
||||
def call_authenticateClient(self, data: dict) -> dict:
|
||||
return self.authenticateClient.call(data)
|
||||
|
||||
def call_getBoundProfilePackage(self, data: dict) -> dict:
|
||||
return self.getBoundProfilePackage.call(data)
|
||||
|
||||
def call_handleNotification(self, data: dict) -> dict:
|
||||
return self.handleNotification.call(data)
|
||||
|
||||
def call_cancelSession(self, data: dict) -> dict:
|
||||
return self.cancelSession.call(data)
|
||||
@@ -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 classs for representing an HTTP[s] API Function."""
|
||||
# the below class variables are expected to be overridden in derived classes
|
||||
|
||||
path = None
|
||||
# dictionary of input parameters. key is parameter name, value is ApiParam class
|
||||
input_params = {}
|
||||
# list of mandatory input parameters
|
||||
input_mandatory = []
|
||||
# dictionary of output parameters. key is parameter name, value is ApiParam class
|
||||
output_params = {}
|
||||
# list of mandatory output parameters (for successful response)
|
||||
output_mandatory = []
|
||||
# expected HTTP status code of the response
|
||||
expected_http_status = 200
|
||||
# the HTTP method used (GET, OPTIONS, HEAD, POST, PUT, PATCH or DELETE)
|
||||
http_method = 'POST'
|
||||
extra_http_req_headers = {}
|
||||
|
||||
def __init__(self, url_prefix: str, func_req_id: Optional[str], session: requests.Session):
|
||||
self.url_prefix = url_prefix
|
||||
self.func_req_id = func_req_id
|
||||
self.session = session
|
||||
|
||||
def encode(self, data: dict, func_call_id: Optional[str] = None) -> dict:
|
||||
"""Validate an encode input dict into JSON-serializable dict for request body."""
|
||||
output = {}
|
||||
if func_call_id:
|
||||
output['header'] = {
|
||||
'functionRequesterIdentifier': self.func_req_id,
|
||||
'functionCallIdentifier': func_call_id
|
||||
}
|
||||
|
||||
for p in self.input_mandatory:
|
||||
if not p in data:
|
||||
raise ValueError('Mandatory input parameter %s missing' % p)
|
||||
for p, v in data.items():
|
||||
p_class = self.input_params.get(p)
|
||||
if not p_class:
|
||||
logger.warning('Unexpected/unsupported input parameter %s=%s', p, v)
|
||||
output[p] = v
|
||||
else:
|
||||
output[p] = p_class.encode(v)
|
||||
return output
|
||||
|
||||
def decode(self, data: dict) -> dict:
|
||||
"""[further] Decode and validate the JSON-Dict of the response body."""
|
||||
output = {}
|
||||
if 'header' in self.output_params:
|
||||
# let's first do the header, it's special
|
||||
if not 'header' in data:
|
||||
raise ValueError('Mandatory output parameter "header" missing')
|
||||
hdr_class = self.output_params.get('header')
|
||||
output['header'] = hdr_class.decode(data['header'])
|
||||
|
||||
if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
|
||||
raise ApiError(output['header']['functionExecutionStatus'])
|
||||
# we can only expect mandatory parameters to be present in case of successful execution
|
||||
for p in self.output_mandatory:
|
||||
if p == 'header':
|
||||
continue
|
||||
if not p in data:
|
||||
raise ValueError('Mandatory output parameter "%s" missing' % p)
|
||||
for p, v in data.items():
|
||||
p_class = self.output_params.get(p)
|
||||
if not p_class:
|
||||
logger.warning('Unexpected/unsupported output parameter "%s"="%s"', p, v)
|
||||
output[p] = v
|
||||
else:
|
||||
output[p] = p_class.decode(v)
|
||||
return output
|
||||
|
||||
def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]:
|
||||
"""Make an API call to the HTTP API endpoint represented by this object.
|
||||
Input data is passed in `data` as json-serializable dict. Output data
|
||||
is returned as json-deserialized dict."""
|
||||
url = self.url_prefix + self.path
|
||||
encoded = json.dumps(self.encode(data, func_call_id))
|
||||
req_headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Protocol': 'gsma/rsp/v2.5.0',
|
||||
}
|
||||
req_headers.update(self.extra_http_req_headers)
|
||||
|
||||
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
|
||||
response = self.session.request(self.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
|
||||
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
|
||||
logger.debug("HTTP RSP: %s" % (response.content))
|
||||
|
||||
if response.status_code != self.expected_http_status:
|
||||
raise HttpStatusError(response)
|
||||
if not response.headers.get('Content-Type').startswith(req_headers['Content-Type']):
|
||||
raise HttpHeaderError(response)
|
||||
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
|
||||
raise HttpHeaderError(response)
|
||||
|
||||
if response.content:
|
||||
return self.decode(response.json())
|
||||
return None
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning) as per SGP22 v3.0"""
|
||||
|
||||
# Implementation of GSMA eSIM RSP (Remote SIM Provisioning)
|
||||
# as per SGP22 v3.0
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -18,12 +19,12 @@
|
||||
|
||||
from typing import Optional
|
||||
import shelve
|
||||
import copyreg
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography import x509
|
||||
from osmocom.utils import b2h
|
||||
from osmocom.tlv import bertlv_parse_one_rawtag, bertlv_return_one_rawtlv
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
from pySim.esim import compile_asn1_subdir
|
||||
|
||||
@@ -34,11 +35,10 @@ class RspSessionState:
|
||||
and subsequently used by further API calls using the same transactionId. The session state
|
||||
is removed either after cancelSession or after notification.
|
||||
TODO: add some kind of time based expiration / garbage collection."""
|
||||
def __init__(self, transactionId: str, serverChallenge: bytes, ci_cert_id: bytes):
|
||||
def __init__(self, transactionId: str, serverChallenge: bytes):
|
||||
self.transactionId = transactionId
|
||||
self.serverChallenge = serverChallenge
|
||||
# used at a later point between API calls
|
||||
self.ci_cert_id = ci_cert_id
|
||||
# used at a later point between API calsl
|
||||
self.euicc_cert: Optional[x509.Certificate] = None
|
||||
self.eum_cert: Optional[x509.Certificate] = None
|
||||
self.eid: Optional[bytes] = None
|
||||
@@ -97,35 +97,4 @@ class RspSessionState:
|
||||
class RspSessionStore(shelve.DbfilenameShelf):
|
||||
"""A derived class as wrapper around the database-backed non-volatile storage 'shelve', in case we might
|
||||
need to extend it in the future. We use it to store RspSessionState objects indexed by transactionId."""
|
||||
|
||||
def extract_euiccSigned1(authenticateServerResponse: bytes) -> bytes:
|
||||
"""Extract the raw, DER-encoded binary euiccSigned1 field from the given AuthenticateServerResponse. This
|
||||
is needed due to the very peculiar SGP.22 notion of signing sections of DER-encoded ASN.1 objects."""
|
||||
rawtag, l, v, remainder = bertlv_parse_one_rawtag(authenticateServerResponse)
|
||||
if len(remainder):
|
||||
raise ValueError('Excess data at end of TLV')
|
||||
if rawtag != 0xbf38:
|
||||
raise ValueError('Unexpected outer tag: %s' % b2h(rawtag))
|
||||
rawtag, l, v1, remainder = bertlv_parse_one_rawtag(v)
|
||||
if rawtag != 0xa0:
|
||||
raise ValueError('Unexpected tag where CHOICE was expected')
|
||||
rawtag, l, tlv2, remainder = bertlv_return_one_rawtlv(v1)
|
||||
if rawtag != 0x30:
|
||||
raise ValueError('Unexpected tag where SEQUENCE was expected')
|
||||
return tlv2
|
||||
|
||||
def extract_euiccSigned2(prepareDownloadResponse: bytes) -> bytes:
|
||||
"""Extract the raw, DER-encoded binary euiccSigned2 field from the given prepareDownloadrResponse. This is
|
||||
needed due to the very peculiar SGP.22 notion of signing sections of DER-encoded ASN.1 objects."""
|
||||
rawtag, l, v, remainder = bertlv_parse_one_rawtag(prepareDownloadResponse)
|
||||
if len(remainder):
|
||||
raise ValueError('Excess data at end of TLV')
|
||||
if rawtag != 0xbf21:
|
||||
raise ValueError('Unexpected outer tag: %s' % b2h(rawtag))
|
||||
rawtag, l, v1, remainder = bertlv_parse_one_rawtag(v)
|
||||
if rawtag != 0xa0:
|
||||
raise ValueError('Unexpected tag where CHOICE was expected')
|
||||
rawtag, l, tlv2, remainder = bertlv_return_one_rawtlv(v1)
|
||||
if rawtag != 0x30:
|
||||
raise ValueError('Unexpected tag where SEQUENCE was expected')
|
||||
return tlv2
|
||||
pass
|
||||
|
||||
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>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import abc
|
||||
import io
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from osmocom.tlv import camel_to_snake
|
||||
from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
|
||||
from pySim.esim.saip import ProfileElement, ProfileElementSequence
|
||||
|
||||
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) -> List[Tuple]:
|
||||
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'."""
|
||||
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):
|
||||
"""Completely replace all fillFileContent of a decoded 'File' with the new_content."""
|
||||
# use [:] to avoid making a copy, as we're doing in-place modification of the list here
|
||||
file[:] = remove_unwanted_tuples_from_list(file, ['fillFileContent', 'fillFileOffset'])
|
||||
file = remove_unwanted_tuples_from_list(file, 'fillFileContent')
|
||||
file.append(('fillFileContent', new_content))
|
||||
return file
|
||||
|
||||
class ClassVarMeta(abc.ABCMeta):
|
||||
"""Metaclass that puts all additional keyword-args into the class. We use this to have one
|
||||
class definition for something like a PIN, and then have derived classes for PIN1, PIN2, ..."""
|
||||
"""Metaclass that puts all additional keyword-args into the class."""
|
||||
def __new__(metacls, name, bases, namespace, **kwargs):
|
||||
#print("Meta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
|
||||
x = super().__new__(metacls, name, bases, namespace)
|
||||
for k, v in kwargs.items():
|
||||
setattr(x, k, v)
|
||||
setattr(x, 'name', camel_to_snake(name))
|
||||
return x
|
||||
|
||||
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
||||
"""Base class representing a part of the eSIM profile that is configurable during the
|
||||
personalization process (with dynamic data from elsewhere)."""
|
||||
def __init__(self, input_value):
|
||||
self.input_value = input_value # the raw input value as given by caller
|
||||
self.value = None # the processed input value (e.g. with check digit) as produced by validate()
|
||||
|
||||
def validate(self):
|
||||
"""Optional validation method. Can be used by derived classes to perform validation
|
||||
of the input value (self.value). Will raise an exception if validation fails."""
|
||||
# default implementation: simply copy input_value over to value
|
||||
self.value = self.input_value
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
def apply(self, pe_seq: ProfileElementSequence):
|
||||
pass
|
||||
|
||||
class Iccid(ConfigurableParameter):
|
||||
"""Configurable ICCID. Expects the value to be a string of decimal digits.
|
||||
If the string of digits is only 18 digits long, a Luhn check digit will be added."""
|
||||
|
||||
def validate(self):
|
||||
# convert to string as it 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)
|
||||
|
||||
"""Configurable ICCID. Expects the value to be in EF.ICCID format."""
|
||||
name = 'iccid'
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
# patch the header
|
||||
pes.get_pe_for_type('header').decoded['iccid'] = h2b(rpad(self.value, 20))
|
||||
# patch the header; FIXME: swap nibbles!
|
||||
pes.get_pe_by_type('header').decoded['iccid'] = self.value
|
||||
# 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):
|
||||
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
|
||||
the last digit of the IMSI."""
|
||||
|
||||
def validate(self):
|
||||
# convert to string as it 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
|
||||
|
||||
"""Configurable IMSI. Expects value to be n EF.IMSI format."""
|
||||
name = 'imsi'
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
imsi_str = self.value
|
||||
# we always use the least significant byte of the IMSI as ACC
|
||||
acc = (1 << int(imsi_str[-1]))
|
||||
# patch ADF.USIM/EF.IMSI
|
||||
for pe in pes.get_pes_for_type('usim'):
|
||||
file_replace_content(pe.decoded['ef-imsi'], h2b(enc_imsi(imsi_str)))
|
||||
file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
|
||||
for pe in pes.get_pes_by_type('usim'):
|
||||
file_replace_content(pe.decoded['ef-imsi'], self.value)
|
||||
# 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:
|
||||
filtered = list(filter(lambda x: x.type == wanted_type, l))
|
||||
assert len(filtered) == 1
|
||||
@@ -209,25 +77,13 @@ def obtain_first_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> Pr
|
||||
return filtered[0]
|
||||
|
||||
class Puk(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
"""Configurable PUK (Pin Unblock Code). String ASCII-encoded digits."""
|
||||
keyReference = None
|
||||
def validate(self):
|
||||
if isinstance(self.input_value, int):
|
||||
self.value = '%08d' % self.input_value
|
||||
else:
|
||||
self.value = self.input_value
|
||||
# FIXME: valid length?
|
||||
if not self.value.isdecimal():
|
||||
raise ValueError('PUK must only contain decimal digits')
|
||||
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
puk = ''.join(['%02x' % (ord(x)) for x in self.value])
|
||||
padded_puk = rpad(puk, 16)
|
||||
mf_pes = pes.pes_by_naa['mf'][0]
|
||||
pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
|
||||
for pukCode in pukCodes.decoded['pukCodes']:
|
||||
if pukCode['keyReference'] == self.keyReference:
|
||||
pukCode['pukValue'] = h2b(padded_puk)
|
||||
pukCode['pukValue'] = self.value
|
||||
return
|
||||
raise ValueError('cannot find pukCode')
|
||||
class Puk1(Puk, keyReference=0x01):
|
||||
@@ -236,52 +92,29 @@ class Puk2(Puk, keyReference=0x81):
|
||||
pass
|
||||
|
||||
class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
"""Configurable PIN (Personal Identification Number). String of digits."""
|
||||
keyReference = None
|
||||
def validate(self):
|
||||
if isinstance(self.input_value, int):
|
||||
self.value = '%04d' % self.input_value
|
||||
else:
|
||||
self.value = self.input_value
|
||||
if len(self.value) < 4 or len(self.value) > 8:
|
||||
raise ValueError('PIN mus be 4..8 digits long')
|
||||
if not self.value.isdecimal():
|
||||
raise ValueError('PIN must only contain decimal digits')
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
pin = ''.join(['%02x' % (ord(x)) for x in self.value])
|
||||
padded_pin = rpad(pin, 16)
|
||||
mf_pes = pes.pes_by_naa['mf'][0]
|
||||
pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
|
||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||
return
|
||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||
if pinCode['keyReference'] == self.keyReference:
|
||||
pinCode['pinValue'] = h2b(padded_pin)
|
||||
pinCode['pinValue'] = self.value
|
||||
return
|
||||
raise ValueError('cannot find pinCode')
|
||||
class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
"""Configurable PIN (Personal Identification Number). String of digits."""
|
||||
keyReference = None
|
||||
def validate(self):
|
||||
if isinstance(self.input_value, int):
|
||||
self.value = '%04d' % self.input_value
|
||||
else:
|
||||
self.value = self.input_value
|
||||
if len(self.value) < 4 or len(self.value) > 8:
|
||||
raise ValueError('PIN mus be 4..8 digits long')
|
||||
if not self.value.isdecimal():
|
||||
raise ValueError('PIN must only contain decimal digits')
|
||||
def _apply_one(self, pe: ProfileElement):
|
||||
pin = ''.join(['%02x' % (ord(x)) for x in self.value])
|
||||
padded_pin = rpad(pin, 16)
|
||||
pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes')
|
||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||
return
|
||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||
if pinCode['keyReference'] == self.keyReference:
|
||||
pinCode['pinValue'] = h2b(padded_pin)
|
||||
pinCode['pinValue'] = self.value
|
||||
return
|
||||
raise ValueError('cannot find pinCode')
|
||||
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
for naa in pes.pes_by_naa:
|
||||
if naa not in ['usim','isim','csim','telecom']:
|
||||
@@ -300,12 +133,7 @@ class Adm2(Pin, keyReference=0x0B):
|
||||
|
||||
|
||||
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
"""Configurable Algorithm parameter."""
|
||||
key = None
|
||||
def validate(self):
|
||||
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
|
||||
raise ValueError('Value must be of bytes-like type')
|
||||
self.value = self.input_value
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
for pe in pes.get_pes_for_type('akaParameter'):
|
||||
algoConfiguration = pe.decoded['algoConfiguration']
|
||||
@@ -318,7 +146,5 @@ class K(AlgoConfig, key='key'):
|
||||
class Opc(AlgoConfig, key='opc'):
|
||||
pass
|
||||
class AlgorithmID(AlgoConfig, key='algorithmID'):
|
||||
def validate(self):
|
||||
if self.input_value not in [1, 2, 3]:
|
||||
raise ValueError('Invalid algorithmID %s' % (self.input_value))
|
||||
self.value = self.input_value
|
||||
pass
|
||||
|
||||
|
||||
@@ -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>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,21 +19,16 @@
|
||||
from pySim.esim.saip import *
|
||||
|
||||
class ProfileError(Exception):
|
||||
"""Raised when a ProfileConstraintChecker finds an error in a file [structure]."""
|
||||
pass
|
||||
|
||||
class ProfileConstraintChecker:
|
||||
"""Base class of a constraint checker for a ProfileElementSequence."""
|
||||
def check(self, pes: ProfileElementSequence):
|
||||
"""Execute all the check_* methods of the ProfileConstraintChecker against the given
|
||||
ProfileElementSequence"""
|
||||
for name in dir(self):
|
||||
if name.startswith('check_'):
|
||||
method = getattr(self, name)
|
||||
method(pes)
|
||||
|
||||
class CheckBasicStructure(ProfileConstraintChecker):
|
||||
"""ProfileConstraintChecker for the basic profile structure constraints."""
|
||||
def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
|
||||
opt_pe = pes.get_pe_for_type(opt)
|
||||
if opt_pe:
|
||||
@@ -43,7 +38,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
# FIXME: check order
|
||||
|
||||
def check_start_and_end(self, pes: ProfileElementSequence):
|
||||
"""Check for mandatory header and end ProfileElements at the right position."""
|
||||
if pes.pe_list[0].type != 'header':
|
||||
raise ProfileError('first element is not header')
|
||||
if pes.pe_list[1].type != 'mf':
|
||||
@@ -53,7 +47,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
raise ProfileError('last element is not end')
|
||||
|
||||
def check_number_of_occurrence(self, pes: ProfileElementSequence):
|
||||
"""Check The number of occurrence of various ProfileElements."""
|
||||
# check for invalid number of occurrences
|
||||
if len(pes.get_pes_for_type('header')) != 1:
|
||||
raise ProfileError('multiple ProfileHeader')
|
||||
@@ -67,7 +60,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
raise ProfileError('multiple PE-%s' % tn.upper())
|
||||
|
||||
def check_optional_ordering(self, pes: ProfileElementSequence):
|
||||
"""Check the ordering of optional PEs following the respective mandatory ones."""
|
||||
# ordering and required depenencies
|
||||
self._is_after_if_exists(pes,'opt-usim', 'usim')
|
||||
self._is_after_if_exists(pes,'opt-isim', 'isim')
|
||||
@@ -100,47 +92,5 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
raise ProfileError('get-identity mandatory, but no usim or isim')
|
||||
if 'profile-a-x25519' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
raise ProfileError('profile-a-x25519 mandatory, but no usim or isim')
|
||||
if 'profile-a-p256' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
if 'profile-a-p256' in m_svcs and not not ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
raise ProfileError('profile-a-p256 mandatory, but no usim or isim')
|
||||
|
||||
def check_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 aki:
|
||||
raise VerifyError('Could not find intermediate certificate for AuthKeyId %s' % b2h(aki))
|
||||
check_signed(c, parent_cert)
|
||||
# if we reach here, we passed (no exception raised)
|
||||
c = parent_cert
|
||||
depth += 1
|
||||
if depth > max_depth:
|
||||
raise VerifyError('Maximum depth %u exceeded while verifying certificate chain' % max_depth)
|
||||
|
||||
|
||||
def ecdsa_dss_to_tr03111(sig: bytes) -> bytes:
|
||||
"""convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes."""
|
||||
r, s = decode_dss_signature(sig)
|
||||
return r.to_bytes(32, 'big') + s.to_bytes(32, 'big')
|
||||
|
||||
|
||||
class CertAndPrivkey:
|
||||
"""A pair of certificate and private key, as used for ECDSA signing."""
|
||||
def __init__(self, required_policy_oid: Optional[x509.ObjectIdentifier] = None,
|
||||
cert: Optional[x509.Certificate] = None, priv_key = None):
|
||||
self.required_policy_oid = required_policy_oid
|
||||
self.cert = cert
|
||||
self.priv_key = priv_key
|
||||
|
||||
def cert_from_der_file(self, path: str):
|
||||
with open(path, 'rb') as f:
|
||||
cert = x509.load_der_x509_certificate(f.read())
|
||||
if self.required_policy_oid:
|
||||
# verify it is the right type of certificate (id-rspRole-dp-auth, id-rspRole-dp-auth-v2, etc.)
|
||||
assert cert_policy_has_oid(cert, self.required_policy_oid)
|
||||
self.cert = cert
|
||||
|
||||
def privkey_from_pem_file(self, path: str, password: Optional[str] = None):
|
||||
with open(path, 'rb') as f:
|
||||
self.priv_key = load_pem_private_key(f.read(), password)
|
||||
|
||||
def ecdsa_sign(self, plaintext: bytes) -> bytes:
|
||||
"""Sign some input-data using an ECDSA signature compliant with SGP.22,
|
||||
which internally refers to Global Platform 2.2 Annex E, which in turn points
|
||||
to BSI TS-03111 which states "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 -*-
|
||||
|
||||
"""
|
||||
Various definitions related to GSMA consumer + IoT eSIM / eUICC
|
||||
Various definitions related to GSMA eSIM / eUICC
|
||||
|
||||
Does *not* implement anything related to M2M eUICC
|
||||
|
||||
Related Specs: GSMA SGP.21, SGP.22, SGP.31, SGP32
|
||||
Related Specs: GSMA SGP.22, GSMA SGP.02, etc.
|
||||
"""
|
||||
|
||||
# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
|
||||
@@ -23,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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pySim.tlv import *
|
||||
from pySim.construct import *
|
||||
from construct import Optional as COptional
|
||||
from construct import *
|
||||
import argparse
|
||||
|
||||
from construct import Array, Struct, FlagsEnum, GreedyRange
|
||||
from cmd2 import cmd2, CommandSet, with_default_category
|
||||
from osmocom.utils import Hexstr
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.utils import Hexstr, SwHexstr, SwMatchstr
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.filesystem import CardADF, CardApplication
|
||||
from pySim.utils import Hexstr, SwHexstr
|
||||
import pySim.global_platform
|
||||
|
||||
# SGP.02 Section 2.2.2
|
||||
class Sgp02Eid(BER_TLV_IE, tag=0x5a):
|
||||
_construct = BcdAdapter(GreedyBytes)
|
||||
|
||||
# patch this into global_platform, to allow 'get_data sgp02_eid' in EF.ECASD
|
||||
pySim.global_platform.DataCollection.possible_nested.append(Sgp02Eid)
|
||||
|
||||
def compute_eid_checksum(eid) -> str:
|
||||
"""Compute and add/replace check digits of an EID value according to GSMA SGP.29 Section 10."""
|
||||
if isinstance(eid, str):
|
||||
if len(eid) == 30:
|
||||
# first pad by 2 digits
|
||||
eid += "00"
|
||||
elif len(eid) == 32:
|
||||
# zero the last two digits
|
||||
eid = eid[:-2] + "00"
|
||||
else:
|
||||
raise ValueError("and EID must be 30 or 32 digits")
|
||||
eid_int = int(eid)
|
||||
elif isinstance(eid, int):
|
||||
eid_int = eid
|
||||
if eid_int % 100:
|
||||
# zero the last two digits
|
||||
eid_int -= eid_int % 100
|
||||
# Using the resulting 32 digits as a decimal integer, compute the remainder of that number on division by
|
||||
# 97, Subtract the remainder from 98, and use the decimal result for the two check digits, if the result
|
||||
# is one digit long, its value SHALL be prefixed by one digit of 0.
|
||||
csum = 98 - (eid_int % 97)
|
||||
eid_int += csum
|
||||
return str(eid_int)
|
||||
|
||||
def verify_eid_checksum(eid) -> bool:
|
||||
"""Verify the check digits of an EID value according to GSMA SGP.29 Section 10."""
|
||||
# Using the 32 digits as a decimal integer, compute the remainder of that number on division by 97. If the
|
||||
# remainder of the division is 1, the verification is successful; otherwise the EID is invalid.
|
||||
return int(eid) % 97 == 1
|
||||
|
||||
class VersionAdapter(Adapter):
|
||||
"""convert an EUICC Version (3-int array) to a textual representation."""
|
||||
|
||||
@@ -91,6 +49,15 @@ AID_ECASD = "A0000005591010FFFFFFFF8900000200"
|
||||
AID_ISD_P_FILE = "A0000005591010FFFFFFFF8900000D00"
|
||||
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):
|
||||
_construct = GreedyBytes
|
||||
|
||||
@@ -120,7 +87,7 @@ class SetDefaultDpAddress(BER_TLV_IE, tag=0xbf3f, nested=[DefaultDpAddress, SetD
|
||||
|
||||
# SGP.22 Section 5.7.7: GetEUICCChallenge
|
||||
class EuiccChallenge(BER_TLV_IE, tag=0x80):
|
||||
_construct = Bytes(16)
|
||||
_construct = HexAdapter(Bytes(16))
|
||||
class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
|
||||
pass
|
||||
|
||||
@@ -128,7 +95,7 @@ class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
|
||||
class SVN(BER_TLV_IE, tag=0x82):
|
||||
_construct = VersionType
|
||||
class SubjectKeyIdentifier(BER_TLV_IE, tag=0x04):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class EuiccCiPkiListForVerification(BER_TLV_IE, tag=0xa9, nested=[SubjectKeyIdentifier]):
|
||||
pass
|
||||
class EuiccCiPkiListForSigning(BER_TLV_IE, tag=0xaa, nested=[SubjectKeyIdentifier]):
|
||||
@@ -140,15 +107,15 @@ class ProfileVersion(BER_TLV_IE, tag=0x81):
|
||||
class EuiccFirmwareVer(BER_TLV_IE, tag=0x83):
|
||||
_construct = VersionType
|
||||
class ExtCardResource(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class UiccCapability(BER_TLV_IE, tag=0x85):
|
||||
_construct = GreedyBytes # FIXME
|
||||
_construct = HexAdapter(GreedyBytes) # FIXME
|
||||
class TS102241Version(BER_TLV_IE, tag=0x86):
|
||||
_construct = VersionType
|
||||
class GlobalPlatformVersion(BER_TLV_IE, tag=0x87):
|
||||
_construct = VersionType
|
||||
class RspCapability(BER_TLV_IE, tag=0x88):
|
||||
_construct = GreedyBytes # FIXME
|
||||
_construct = HexAdapter(GreedyBytes) # FIXME
|
||||
class EuiccCategory(BER_TLV_IE, tag=0x8b):
|
||||
_construct = Enum(Int8ub, other=0, basicEuicc=1, mediumEuicc=2, contactlessEuicc=3)
|
||||
class PpVersion(BER_TLV_IE, tag=0x04):
|
||||
@@ -177,7 +144,7 @@ class ProfileMgmtOperation(BER_TLV_IE, tag=0x81):
|
||||
class ListNotificationReq(BER_TLV_IE, tag=0xbf28, nested=[ProfileMgmtOperation]):
|
||||
pass
|
||||
class SeqNumber(BER_TLV_IE, tag=0x80):
|
||||
_construct = Asn1DerInteger()
|
||||
_construct = GreedyInteger()
|
||||
class NotificationAddress(BER_TLV_IE, tag=0x0c):
|
||||
_construct = Utf8Adapter(GreedyBytes)
|
||||
class Iccid(BER_TLV_IE, tag=0x5a):
|
||||
@@ -211,7 +178,7 @@ class TagList(BER_TLV_IE, tag=0x5c):
|
||||
class ProfileInfoListReq(BER_TLV_IE, tag=0xbf2d, nested=[TagList]): # FIXME: SearchCriteria
|
||||
pass
|
||||
class IsdpAid(BER_TLV_IE, tag=0x4f):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class ProfileState(BER_TLV_IE, tag=0x9f70):
|
||||
_construct = Enum(Int8ub, disabled=0, enabled=1)
|
||||
class ProfileNickname(BER_TLV_IE, tag=0x90):
|
||||
@@ -268,20 +235,9 @@ class DeleteProfileReq(BER_TLV_IE, tag=0xbf33, nested=[IsdpAid, Iccid]):
|
||||
class DeleteProfileResp(BER_TLV_IE, tag=0xbf33, nested=[DeleteResult]):
|
||||
pass
|
||||
|
||||
# SGP.22 Section 5.7.19: EuiccMemoryReset
|
||||
class ResetOptions(BER_TLV_IE, tag=0x82):
|
||||
_construct = FlagsEnum(Byte, deleteOperationalProfiles=0x80, deleteFieldLoadedTestProfiles=0x40,
|
||||
resetDefaultSmdpAddress=0x20)
|
||||
class ResetResult(BER_TLV_IE, tag=0x80):
|
||||
_construct = Enum(Int8ub, ok=0, nothingToDelete=1, undefinedError=127)
|
||||
class EuiccMemoryResetReq(BER_TLV_IE, tag=0xbf34, nested=[ResetOptions]):
|
||||
pass
|
||||
class EuiccMemoryResetResp(BER_TLV_IE, tag=0xbf34, nested=[ResetResult]):
|
||||
pass
|
||||
|
||||
# SGP.22 Section 5.7.20 GetEID
|
||||
class EidValue(BER_TLV_IE, tag=0x5a):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class GetEuiccData(BER_TLV_IE, tag=0xbf3e, nested=[TagList, EidValue]):
|
||||
pass
|
||||
|
||||
@@ -302,7 +258,7 @@ class EumCertificate(BER_TLV_IE, tag=0xa5):
|
||||
_construct = GreedyBytes
|
||||
class EuiccCertificate(BER_TLV_IE, tag=0xa6):
|
||||
_construct = GreedyBytes
|
||||
class GetCertsError(BER_TLV_IE, tag=0x81):
|
||||
class GetCertsError(BER_TLV_IE, tag=0x80):
|
||||
_construct = Enum(Int8ub, invalidCiPKId=1, undefinedError=127)
|
||||
class GetCertsResp(BER_TLV_IE, tag=0xbf56, nested=[EumCertificate, EuiccCertificate, GetCertsError]):
|
||||
pass
|
||||
@@ -315,9 +271,9 @@ class EimFqdn(BER_TLV_IE, tag=0x81):
|
||||
class EimIdType(BER_TLV_IE, tag=0x82):
|
||||
_construct = Enum(Int8ub, eimIdTypeOid=1, eimIdTypeFqdn=2, eimIdTypeProprietary=3)
|
||||
class CounterValue(BER_TLV_IE, tag=0x83):
|
||||
_construct = Asn1DerInteger()
|
||||
_construct = GreedyInteger
|
||||
class AssociationToken(BER_TLV_IE, tag=0x84):
|
||||
_construct = Asn1DerInteger()
|
||||
_construct = GreedyInteger
|
||||
class EimSupportedProtocol(BER_TLV_IE, tag=0x87):
|
||||
_construct = Enum(Int8ub, eimRetrieveHttps=0, eimRetrieveCoaps=1, eimInjectHttps=2, eimInjectCoaps=3,
|
||||
eimProprietary=4)
|
||||
@@ -330,24 +286,21 @@ class EimConfigurationDataSeq(BER_TLV_IE, tag=0xa0, nested=[EimConfigurationData
|
||||
class GetEimConfigurationData(BER_TLV_IE, tag=0xbf55, nested=[EimConfigurationDataSeq]):
|
||||
pass
|
||||
|
||||
class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
def __init__(self):
|
||||
super().__init__(name='ADF.ISD-R', aid=AID_ISD_R,
|
||||
desc='ISD-R (Issuer Security Domain Root) Application')
|
||||
self.adf.decode_select_response = self.decode_select_response
|
||||
self.adf.shell_commands += [self.AddlShellCommands()]
|
||||
# we attempt to retrieve ISD-R key material from CardKeyProvider identified by EID
|
||||
self.adf.scp_key_identity = 'EID'
|
||||
class ADF_ISDR(CardADF):
|
||||
def __init__(self, aid=AID_ISD_R, name='ADF.ISD-R', fid=None, sfid=None,
|
||||
desc='ISD-R (Issuer Security Domain Root) Application'):
|
||||
super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
@staticmethod
|
||||
def store_data(scc: SimCardCommands, tx_do: Hexstr, exp_sw: SwMatchstr ="9000") -> Tuple[Hexstr, SwHexstr]:
|
||||
def store_data(scc: SimCardCommands, tx_do: Hexstr) -> Tuple[Hexstr, SwHexstr]:
|
||||
"""Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22.
|
||||
Only single-block store supported for now."""
|
||||
capdu = '80E29100%02x%s00' % (len(tx_do)//2, tx_do)
|
||||
return scc.send_apdu_checksw(capdu, exp_sw)
|
||||
capdu = '%sE29100%02x%s' % (scc.cla4lchan('80'), len(tx_do)//2, tx_do)
|
||||
return scc._tp.send_apdu_checksw(capdu)
|
||||
|
||||
@staticmethod
|
||||
def store_data_tlv(scc: SimCardCommands, cmd_do, resp_cls, exp_sw: SwMatchstr = '9000'):
|
||||
def store_data_tlv(scc: SimCardCommands, cmd_do, resp_cls, exp_sw='9000'):
|
||||
"""Transceive STORE DATA APDU with the card, transparently encoding the command data from TLV
|
||||
and decoding the response data tlv."""
|
||||
if cmd_do:
|
||||
@@ -357,7 +310,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
return ValueError('DO > 255 bytes not supported yet')
|
||||
else:
|
||||
cmd_do_enc = b''
|
||||
(data, _sw) = CardApplicationISDR.store_data(scc, b2h(cmd_do_enc), exp_sw=exp_sw)
|
||||
(data, sw) = ADF_ISDR.store_data(scc, b2h(cmd_do_enc))
|
||||
if data:
|
||||
if resp_cls:
|
||||
resp_do = resp_cls()
|
||||
@@ -368,13 +321,6 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_eid(scc: SimCardCommands) -> str:
|
||||
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
|
||||
ged = CardApplicationISDR.store_data_tlv(scc, ged_cmd, GetEuiccData)
|
||||
d = ged.to_dict()
|
||||
return b2h(flatten_dict_lists(d['get_euicc_data'])['eid_value'])
|
||||
|
||||
def decode_select_response(self, data_hex: Hexstr) -> object:
|
||||
t = FciTemplate()
|
||||
t.from_tlv(h2b(data_hex))
|
||||
@@ -390,11 +336,11 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
@cmd2.with_argparser(es10x_store_data_parser)
|
||||
def do_es10x_store_data(self, opts):
|
||||
"""Perform a raw STORE DATA command as defined for the ES10x eUICC interface."""
|
||||
(_data, _sw) = CardApplicationISDR.store_data(self._cmd.lchan.scc, opts.TX_DO)
|
||||
(data, sw) = 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."""
|
||||
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()
|
||||
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):
|
||||
"""Perform an ES10a SetDefaultDpAddress function."""
|
||||
sdda_cmd = SetDefaultDpAddress(children=[DefaultDpAddress(decoded=opts.DP_ADDRESS)])
|
||||
sdda = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sdda_cmd, SetDefaultDpAddress)
|
||||
sdda = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, sdda_cmd, SetDefaultDpAddress)
|
||||
d = sdda.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['set_default_dp_address']))
|
||||
|
||||
def do_get_euicc_challenge(self, _opts):
|
||||
def do_get_euicc_challenge(self, opts):
|
||||
"""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()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_challenge']))
|
||||
|
||||
def do_get_euicc_info1(self, _opts):
|
||||
def do_get_euicc_info1(self, opts):
|
||||
"""Perform an ES10b GetEUICCInfo (1) function."""
|
||||
ei1 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo1(), EuiccInfo1)
|
||||
ei1 = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo1(), EuiccInfo1)
|
||||
d = ei1.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['euicc_info1']))
|
||||
|
||||
def do_get_euicc_info2(self, _opts):
|
||||
def do_get_euicc_info2(self, opts):
|
||||
"""Perform an ES10b GetEUICCInfo (2) function."""
|
||||
ei2 = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo2(), EuiccInfo2)
|
||||
ei2 = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, EuiccInfo2(), EuiccInfo2)
|
||||
d = ei2.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['euicc_info2']))
|
||||
|
||||
def do_list_notification(self, _opts):
|
||||
def do_list_notification(self, opts):
|
||||
"""Perform an ES10b ListNotification function."""
|
||||
ln = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
|
||||
ln = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ListNotificationReq(), ListNotificationResp)
|
||||
d = ln.to_dict()
|
||||
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):
|
||||
"""Perform an ES10b RemoveNotificationFromList function."""
|
||||
rn_cmd = NotificationSentReq(children=[SeqNumber(decoded=opts.SEQ_NR)])
|
||||
rn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, rn_cmd, NotificationSentResp)
|
||||
rn = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, rn_cmd, NotificationSentResp)
|
||||
d = rn.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['notification_sent_resp']))
|
||||
|
||||
def do_get_profiles_info(self, _opts):
|
||||
def do_get_profiles_info(self, opts):
|
||||
"""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()
|
||||
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."""
|
||||
if opts.isdp_aid:
|
||||
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
|
||||
elif opts.iccid:
|
||||
if opts.iccid:
|
||||
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
|
||||
else:
|
||||
# this is guaranteed by argparse; but we need this to make pylint happy
|
||||
raise ValueError('Either ISD-P AID or ICCID must be given')
|
||||
ep_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
|
||||
ep_cmd = EnableProfileReq(children=ep_cmd_contents)
|
||||
ep = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ep_cmd, EnableProfileResp)
|
||||
ep = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ep_cmd, EnableProfileResp)
|
||||
d = ep.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['enable_profile_resp']))
|
||||
|
||||
@@ -483,14 +426,11 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
"""Perform an ES10c DisableProfile function."""
|
||||
if opts.isdp_aid:
|
||||
p_id = ProfileIdentifier(children=[IsdpAid(decoded=opts.isdp_aid)])
|
||||
elif opts.iccid:
|
||||
if opts.iccid:
|
||||
p_id = ProfileIdentifier(children=[Iccid(decoded=opts.iccid)])
|
||||
else:
|
||||
# this is guaranteed by argparse; but we need this to make pylint happy
|
||||
raise ValueError('Either ISD-P AID or ICCID must be given')
|
||||
dp_cmd_contents = [p_id, RefreshFlag(decoded=opts.refresh_required)]
|
||||
dp_cmd = DisableProfileReq(children=dp_cmd_contents)
|
||||
dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DisableProfileResp)
|
||||
dp = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DisableProfileResp)
|
||||
d = dp.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['disable_profile_resp']))
|
||||
|
||||
@@ -504,52 +444,26 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
"""Perform an ES10c DeleteProfile function."""
|
||||
if opts.isdp_aid:
|
||||
p_id = IsdpAid(decoded=opts.isdp_aid)
|
||||
elif opts.iccid:
|
||||
if opts.iccid:
|
||||
p_id = Iccid(decoded=opts.iccid)
|
||||
else:
|
||||
# this is guaranteed by argparse; but we need this to make pylint happy
|
||||
raise ValueError('Either ISD-P AID or ICCID must be given')
|
||||
dp_cmd_contents = [p_id]
|
||||
dp_cmd = DeleteProfileReq(children=dp_cmd_contents)
|
||||
dp = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DeleteProfileResp)
|
||||
dp = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, dp_cmd, DeleteProfileResp)
|
||||
d = dp.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['delete_profile_resp']))
|
||||
|
||||
mem_res_parser = argparse.ArgumentParser()
|
||||
mem_res_parser.add_argument('--delete-operational', action='store_true',
|
||||
help='Delete all operational profiles')
|
||||
mem_res_parser.add_argument('--delete-test-field-installed', action='store_true',
|
||||
help='Delete all test profiles, except pre-installed ones')
|
||||
mem_res_parser.add_argument('--reset-smdp-address', action='store_true',
|
||||
help='Reset the SM-DP+ address')
|
||||
|
||||
@cmd2.with_argparser(mem_res_parser)
|
||||
def do_euicc_memory_reset(self, opts):
|
||||
"""Perform an ES10c eUICCMemoryReset function. This will permanently delete the selected subset of
|
||||
profiles from the eUICC."""
|
||||
flags = {}
|
||||
if opts.delete_operational:
|
||||
flags['deleteOperationalProfiles'] = True
|
||||
if opts.delete_test_field_installed:
|
||||
flags['deleteFieldLoadedTestProfiles'] = True
|
||||
if opts.reset_smdp_address:
|
||||
flags['resetDefaultSmdpAddress'] = True
|
||||
|
||||
mr_cmd = EuiccMemoryResetReq(children=[ResetOptions(decoded=flags)])
|
||||
mr = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, mr_cmd, EuiccMemoryResetResp)
|
||||
d = mr.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['euicc_memory_reset_resp']))
|
||||
|
||||
def do_get_eid(self, _opts):
|
||||
def do_get_eid(self, opts):
|
||||
"""Perform an ES10c GetEID function."""
|
||||
(data, sw) = ADF_ISDR.store_data(self._cmd.lchan.scc, 'BF3E035C015A')
|
||||
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
|
||||
ged = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
|
||||
ged = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, ged_cmd, GetEuiccData)
|
||||
d = ged.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['get_euicc_data']))
|
||||
|
||||
set_nickname_parser = argparse.ArgumentParser()
|
||||
set_nickname_parser.add_argument('--profile-nickname', help='Nickname of the profile')
|
||||
set_nickname_parser.add_argument('ICCID', help='ICCID of the profile whose nickname to set')
|
||||
set_nickname_parser.add_argument('--profile-nickname', help='Nickname of the profile')
|
||||
|
||||
@cmd2.with_argparser(set_nickname_parser)
|
||||
def do_set_nickname(self, opts):
|
||||
@@ -557,78 +471,46 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
nickname = opts.profile_nickname or ''
|
||||
sn_cmd_contents = [Iccid(decoded=opts.ICCID), ProfileNickname(decoded=nickname)]
|
||||
sn_cmd = SetNicknameReq(children=sn_cmd_contents)
|
||||
sn = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, sn_cmd, SetNicknameResp)
|
||||
sn = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, sn_cmd, SetNicknameResp)
|
||||
d = sn.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['set_nickname_resp']))
|
||||
|
||||
def do_get_certs(self, _opts):
|
||||
def do_get_certs(self, opts):
|
||||
"""Perform an ES10c GetCerts() function on an IoT eUICC."""
|
||||
gc = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
|
||||
gc = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetCertsReq(), GetCertsResp)
|
||||
d = gc.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['get_certs_resp']))
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['get_certficiates_resp']))
|
||||
|
||||
def do_get_eim_configuration_data(self, _opts):
|
||||
def do_get_eim_configuration_data(self, opts):
|
||||
"""Perform an ES10b GetEimConfigurationData function on an Iot eUICC."""
|
||||
gec = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
|
||||
GetEimConfigurationData)
|
||||
gec = ADF_ISDR.store_data_tlv(self._cmd.lchan.scc, GetEimConfigurationData(),
|
||||
GetEimConfigurationData)
|
||||
d = gec.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['get_eim_configuration_data']))
|
||||
|
||||
class CardApplicationECASD(pySim.global_platform.CardApplicationSD):
|
||||
|
||||
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:
|
||||
t = FciTemplate()
|
||||
t.from_tlv(h2b(data_hex))
|
||||
d = t.to_dict()
|
||||
return flatten_dict_lists(d['fci_template'])
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='ADF.ECASD', aid=AID_ECASD,
|
||||
desc='ECASD (eUICC Controlling Authority Security Domain) Application')
|
||||
self.adf.decode_select_response = self.decode_select_response
|
||||
self.adf.shell_commands += [self.AddlShellCommands()]
|
||||
# we attempt to retrieve ECASD key material from CardKeyProvider identified by EID
|
||||
self.adf.scp_key_identity = 'EID'
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
pass
|
||||
|
||||
class CardProfileEuiccSGP32(CardProfileUICC):
|
||||
ORDER = 5
|
||||
|
||||
|
||||
class CardApplicationISDR(CardApplication):
|
||||
def __init__(self):
|
||||
super().__init__(name='IoT eUICC (SGP.32)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
# try a command only supported by SGP.32
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ISD_R)
|
||||
CardApplicationISDR.store_data_tlv(scc, GetCertsReq(), GetCertsResp)
|
||||
|
||||
class CardProfileEuiccSGP22(CardProfileUICC):
|
||||
ORDER = 6
|
||||
super().__init__('ISD-R', adf=ADF_ISDR(), sw=sw_isdr)
|
||||
|
||||
class CardApplicationECASD(CardApplication):
|
||||
def __init__(self):
|
||||
super().__init__(name='Consumer eUICC (SGP.22)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
# try to read EID from ISD-R
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ISD_R)
|
||||
eid = CardApplicationISDR.get_eid(scc)
|
||||
# TODO: Store EID identity?
|
||||
|
||||
class CardProfileEuiccSGP02(CardProfileUICC):
|
||||
ORDER = 7
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='M2M eUICC (SGP.02)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ECASD)
|
||||
scc.get_data(0x5a)
|
||||
# TODO: Store EID identity?
|
||||
super().__init__('ECASD', adf=ADF_ECASD(), sw=sw_isdr)
|
||||
|
||||
@@ -24,14 +24,17 @@
|
||||
|
||||
class NoCardError(Exception):
|
||||
"""No card was found in the reader."""
|
||||
pass
|
||||
|
||||
|
||||
class ProtocolError(Exception):
|
||||
"""Some kind of protocol level error interfacing with the card."""
|
||||
pass
|
||||
|
||||
|
||||
class ReaderError(Exception):
|
||||
"""Some kind of general error with the card reader."""
|
||||
pass
|
||||
|
||||
|
||||
class SwMatchError(Exception):
|
||||
@@ -49,18 +52,9 @@ class SwMatchError(Exception):
|
||||
self.sw_expected = sw_expected
|
||||
self.rs = rs
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
def __str__(self):
|
||||
if self.rs and self.rs.lchan[0]:
|
||||
r = self.rs.lchan[0].interpret_sw(self.sw_actual)
|
||||
if r:
|
||||
return "%s - %s" % (r[0], r[1])
|
||||
return ''
|
||||
|
||||
def __str__(self):
|
||||
description = self.description
|
||||
if description:
|
||||
description = ": " + description
|
||||
else:
|
||||
description = "."
|
||||
return "SW match failed! Expected %s and got %s%s" % (self.sw_expected, self.sw_actual, description)
|
||||
return "SW match failed! Expected %s and got %s: %s - %s" % (self.sw_expected, self.sw_actual, r[0], r[1])
|
||||
return "SW match failed! Expected %s and got %s." % (self.sw_expected, self.sw_actual)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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 upport
|
||||
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
|
||||
order to describe the files specified in UIC Reference P38 T 9001 5.0 "FFFIS for GSM-R SIM Cards"
|
||||
"""
|
||||
# without this, pylint will fail when inner classes are used
|
||||
# within the 'nested' kwarg of our TlvMeta metaclass on python 3.7 :(
|
||||
# pylint: disable=undefined-variable
|
||||
|
||||
#
|
||||
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
|
||||
@@ -25,13 +28,16 @@ order to describe the files specified in UIC Reference P38 T 9001 5.0 "FFFIS for
|
||||
|
||||
|
||||
from pySim.utils import *
|
||||
#from pySim.tlv import *
|
||||
from struct import pack, unpack
|
||||
from construct import Struct, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
|
||||
from construct import *
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import *
|
||||
from pySim.construct import *
|
||||
import enum
|
||||
|
||||
from pySim.profile import CardProfileAddon
|
||||
from pySim.filesystem import *
|
||||
import pySim.ts_51_011
|
||||
|
||||
######################################################################
|
||||
# DF.EIRENE (FFFIS for GSM-R SIM Cards)
|
||||
@@ -71,15 +77,15 @@ class PlConfAdapter(Adapter):
|
||||
num = int(obj) & 0x7
|
||||
if num == 0:
|
||||
return 'None'
|
||||
if num == 1:
|
||||
elif num == 1:
|
||||
return 4
|
||||
if num == 2:
|
||||
elif num == 2:
|
||||
return 3
|
||||
if num == 3:
|
||||
elif num == 3:
|
||||
return 2
|
||||
if num == 4:
|
||||
elif num == 4:
|
||||
return 1
|
||||
if num == 5:
|
||||
elif num == 5:
|
||||
return 0
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
@@ -88,13 +94,13 @@ class PlConfAdapter(Adapter):
|
||||
obj = int(obj)
|
||||
if obj == 4:
|
||||
return 1
|
||||
if obj == 3:
|
||||
elif obj == 3:
|
||||
return 2
|
||||
if obj == 2:
|
||||
elif obj == 2:
|
||||
return 3
|
||||
if obj == 1:
|
||||
elif obj == 1:
|
||||
return 4
|
||||
if obj == 0:
|
||||
elif obj == 0:
|
||||
return 5
|
||||
|
||||
|
||||
@@ -105,19 +111,19 @@ class PlCallAdapter(Adapter):
|
||||
num = int(obj) & 0x7
|
||||
if num == 0:
|
||||
return 'None'
|
||||
if num == 1:
|
||||
elif num == 1:
|
||||
return 4
|
||||
if num == 2:
|
||||
elif num == 2:
|
||||
return 3
|
||||
if num == 3:
|
||||
elif num == 3:
|
||||
return 2
|
||||
if num == 4:
|
||||
elif num == 4:
|
||||
return 1
|
||||
if num == 5:
|
||||
elif num == 5:
|
||||
return 0
|
||||
if num == 6:
|
||||
elif num == 6:
|
||||
return 'B'
|
||||
if num == 7:
|
||||
elif num == 7:
|
||||
return 'A'
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
@@ -125,17 +131,17 @@ class PlCallAdapter(Adapter):
|
||||
return 0
|
||||
if obj == 4:
|
||||
return 1
|
||||
if obj == 3:
|
||||
elif obj == 3:
|
||||
return 2
|
||||
if obj == 2:
|
||||
elif obj == 2:
|
||||
return 3
|
||||
if obj == 1:
|
||||
elif obj == 1:
|
||||
return 4
|
||||
if obj == 0:
|
||||
elif obj == 0:
|
||||
return 5
|
||||
if obj == 'B':
|
||||
elif obj == 'B':
|
||||
return 6
|
||||
if obj == 'A':
|
||||
elif obj == 'A':
|
||||
return 7
|
||||
|
||||
|
||||
@@ -184,13 +190,13 @@ class EF_CallconfI(LinFixedEF):
|
||||
class EF_Shunting(TransparentEF):
|
||||
"""Section 7.6"""
|
||||
_test_de_encode = [
|
||||
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": h2b("f8ffffff000000") } ),
|
||||
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": "f8ffffff000000" } ),
|
||||
]
|
||||
def __init__(self):
|
||||
super().__init__(fid='6ff4', sfid=None,
|
||||
name='EF.Shunting', desc='Shunting', size=(8, 8))
|
||||
self._construct = Struct('common_gid'/Int8ub,
|
||||
'shunting_gid'/Bytes(7))
|
||||
'shunting_gid'/HexAdapter(Bytes(7)))
|
||||
|
||||
|
||||
class EF_GsmrPLMN(LinFixedEF):
|
||||
@@ -199,13 +205,13 @@ class EF_GsmrPLMN(LinFixedEF):
|
||||
( "22f860f86f8d6f8e01", { "plmn": "228-06", "class_of_network": {
|
||||
"supported": { "vbs": True, "vgcs": True, "emlpp": True,
|
||||
"fn": True, "eirene": True }, "preference": 0 },
|
||||
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
|
||||
"ic_table_ref": h2b("01") } ),
|
||||
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
|
||||
"ic_table_ref": "01" } ),
|
||||
( "22f810416f8d6f8e02", { "plmn": "228-01", "class_of_network": {
|
||||
"supported": { "vbs": False, "vgcs": False, "emlpp": False,
|
||||
"fn": True, "eirene": False }, "preference": 1 },
|
||||
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
|
||||
"ic_table_ref": h2b("02") } ),
|
||||
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
|
||||
"ic_table_ref": "02" } ),
|
||||
]
|
||||
def __init__(self):
|
||||
super().__init__(fid='6ff5', sfid=None, name='EF.GsmrPLMN',
|
||||
@@ -213,24 +219,24 @@ class EF_GsmrPLMN(LinFixedEF):
|
||||
self._construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
|
||||
'class_of_network'/BitStruct('supported'/FlagsEnum(BitsInteger(5), vbs=1, vgcs=2, emlpp=4, fn=8, eirene=16),
|
||||
'preference'/BitsInteger(3)),
|
||||
'ic_incoming_ref_tbl'/Bytes(2),
|
||||
'outgoing_ref_tbl'/Bytes(2),
|
||||
'ic_table_ref'/Bytes(1))
|
||||
'ic_incoming_ref_tbl'/HexAdapter(Bytes(2)),
|
||||
'outgoing_ref_tbl'/HexAdapter(Bytes(2)),
|
||||
'ic_table_ref'/HexAdapter(Bytes(1)))
|
||||
|
||||
|
||||
class EF_IC(LinFixedEF):
|
||||
"""Section 7.8"""
|
||||
_test_de_encode = [
|
||||
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": h2b("6f8e"),
|
||||
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": "6f8e",
|
||||
"ic_decision_value": "041f", "network_string_table_index": 1 } ),
|
||||
( "ffffffffffffff", { "next_table_type": "empty", "id_of_next_table": h2b("ffff"),
|
||||
( "ffffffffffffff", { "next_table_type": "empty", "id_of_next_table": "ffff",
|
||||
"ic_decision_value": "ffff", "network_string_table_index": 65535 } ),
|
||||
]
|
||||
def __init__(self):
|
||||
super().__init__(fid='6f8d', sfid=None, name='EF.IC',
|
||||
desc='International Code', rec_len=(7, 7))
|
||||
self._construct = Struct('next_table_type'/NextTableType,
|
||||
'id_of_next_table'/Bytes(2),
|
||||
'id_of_next_table'/HexAdapter(Bytes(2)),
|
||||
'ic_decision_value'/BcdAdapter(Bytes(2)),
|
||||
'network_string_table_index'/Int16ub)
|
||||
|
||||
@@ -252,18 +258,18 @@ class EF_NW(LinFixedEF):
|
||||
class EF_Switching(LinFixedEF):
|
||||
"""Section 8.4"""
|
||||
_test_de_encode = [
|
||||
( "f26f87f0ff00", { "next_table_type": "num_dial_digits", "id_of_next_table": h2b("6f87"),
|
||||
( "f26f87f0ff00", { "next_table_type": "num_dial_digits", "id_of_next_table": "6f87",
|
||||
"decision_value": "0fff", "string_table_index": 0 } ),
|
||||
( "f06f8ff1ff01", { "next_table_type": "decision", "id_of_next_table": h2b("6f8f"),
|
||||
( "f06f8ff1ff01", { "next_table_type": "decision", "id_of_next_table": "6f8f",
|
||||
"decision_value": "1fff", "string_table_index": 1 } ),
|
||||
( "f16f89f5ff05", { "next_table_type": "predefined", "id_of_next_table": h2b("6f89"),
|
||||
( "f16f89f5ff05", { "next_table_type": "predefined", "id_of_next_table": "6f89",
|
||||
"decision_value": "5fff", "string_table_index": 5 } ),
|
||||
]
|
||||
def __init__(self, fid='1234', name='Switching', desc=None):
|
||||
super().__init__(fid=fid, sfid=None,
|
||||
name=name, desc=desc, rec_len=(6, 6))
|
||||
self._construct = Struct('next_table_type'/NextTableType,
|
||||
'id_of_next_table'/Bytes(2),
|
||||
'id_of_next_table'/HexAdapter(Bytes(2)),
|
||||
'decision_value'/BcdAdapter(Bytes(2)),
|
||||
'string_table_index'/Int8ub)
|
||||
|
||||
@@ -271,12 +277,12 @@ class EF_Switching(LinFixedEF):
|
||||
class EF_Predefined(LinFixedEF):
|
||||
"""Section 8.5"""
|
||||
_test_de_encode = [
|
||||
( "f26f85", 1, { "next_table_type": "num_dial_digits", "id_of_next_table": h2b("6f85") } ),
|
||||
( "f26f85", 1, { "next_table_type": "num_dial_digits", "id_of_next_table": "6f85" } ),
|
||||
( "f0ffc8", 2, { "predefined_value1": "0fff", "string_table_index1": 200 } ),
|
||||
]
|
||||
# header and other records have different structure. WTF !?!
|
||||
construct_first = Struct('next_table_type'/NextTableType,
|
||||
'id_of_next_table'/Bytes(2))
|
||||
'id_of_next_table'/HexAdapter(Bytes(2)))
|
||||
construct_others = Struct('predefined_value1'/BcdAdapter(Bytes(2)),
|
||||
'string_table_index1'/Int8ub)
|
||||
|
||||
@@ -290,7 +296,7 @@ class EF_Predefined(LinFixedEF):
|
||||
else:
|
||||
return parse_construct(self.construct_others, raw_bin_data)
|
||||
|
||||
def _encode_record_bin(self, abstract_data : dict, record_nr : int, **kwargs) -> bytearray:
|
||||
def _encode_record_bin(self, abstract_data : dict, record_nr : int) -> bytearray:
|
||||
r = None
|
||||
if record_nr == 1:
|
||||
r = self.construct_first.build(abstract_data)
|
||||
@@ -301,13 +307,13 @@ class EF_Predefined(LinFixedEF):
|
||||
class EF_DialledVals(TransparentEF):
|
||||
"""Section 8.6"""
|
||||
_test_de_encode = [
|
||||
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": h2b("ffff"), "dialed_digits": "22" } ),
|
||||
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": h2b("6f88"), "dialed_digits": "58" }),
|
||||
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": "ffff", "dialed_digits": "22" } ),
|
||||
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": "6f88", "dialed_digits": "58" }),
|
||||
]
|
||||
def __init__(self, fid='1234', name='DialledVals', desc=None):
|
||||
super().__init__(fid=fid, sfid=None, name=name, desc=desc, size=(4, 4))
|
||||
self._construct = Struct('next_table_type'/NextTableType,
|
||||
'id_of_next_table'/Bytes(2),
|
||||
'id_of_next_table'/HexAdapter(Bytes(2)),
|
||||
'dialed_digits'/BcdAdapter(Bytes(1)))
|
||||
|
||||
|
||||
|
||||
214
pySim/gsmtap.py
Normal file
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/>.
|
||||
"""
|
||||
|
||||
from construct import GreedyString
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
from construct import *
|
||||
from pySim.construct import *
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.tlv import *
|
||||
|
||||
# Table 91 + Section 8.2.1.2
|
||||
class ApplicationId(BER_TLV_IE, tag=0x4f):
|
||||
|
||||
@@ -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 executeable 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.
|
||||
|
||||
As pySim-sell has the ability to represent SIM files as JSON strings,
|
||||
@@ -5,8 +10,6 @@ adding JSONpath allows us to conveniently modify individual sub-fields
|
||||
of a file or record in its JSON representation.
|
||||
"""
|
||||
|
||||
import jsonpath_ng
|
||||
|
||||
# (C) 2021 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
################################################################################
|
||||
|
||||
import abc
|
||||
from smartcard.util import toBytes
|
||||
from pytlv.TLV import *
|
||||
|
||||
from pySim.cards import SimCardBase, UiccCardBase
|
||||
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi
|
||||
from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi, dec_msisdn, enc_msisdn
|
||||
from pySim.utils import enc_plmn, get_addr_type
|
||||
from pySim.utils import is_hex, h2b, b2h, h2s, s2h, lpad, rpad
|
||||
from pySim.legacy.utils import enc_ePDGSelection, format_xplmn_w_act, format_xplmn, dec_st, enc_st
|
||||
from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv, dec_msisdn, enc_msisdn
|
||||
from pySim.legacy.utils import format_ePDGSelection, dec_addr_tlv, enc_addr_tlv
|
||||
from pySim.legacy.ts_51_011 import EF, DF
|
||||
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
|
||||
from pySim.legacy.ts_31_103 import EF_ISIM_ADF_map
|
||||
@@ -752,7 +753,7 @@ class GrcardSim(SimCard):
|
||||
|
||||
# Set the Ki using proprietary command
|
||||
pdu = '80d4020010' + p['ki']
|
||||
data, sw = self._scc.send_apdu(pdu)
|
||||
data, sw = self._scc._tp.send_apdu(pdu)
|
||||
|
||||
# EF.HPLMN
|
||||
r = self._scc.select_path(['3f00', '7f20', '6f30'])
|
||||
@@ -780,7 +781,7 @@ class SysmoSIMgr1(GrcardSim):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b991800118822334455667760":
|
||||
if scc.get_atr() == toBytes("3B 99 18 00 11 88 22 33 44 55 66 77 60"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -802,7 +803,7 @@ class SysmoUSIMgr1(UsimCard):
|
||||
# TODO: check if verify_chv could be used or what it needs
|
||||
# self._scc.verify_chv(0x0A, [0x33,0x32,0x32,0x31,0x33,0x32,0x33,0x32])
|
||||
# Unlock the card..
|
||||
data, sw = self._scc.send_apdu_checksw(
|
||||
data, sw = self._scc._tp.send_apdu_checksw(
|
||||
"0020000A083332323133323332")
|
||||
|
||||
# TODO: move into SimCardCommands
|
||||
@@ -811,7 +812,7 @@ class SysmoUSIMgr1(UsimCard):
|
||||
enc_iccid(p['iccid']) + # 10b ICCID
|
||||
enc_imsi(p['imsi']) # 9b IMSI_len + id_type(9) + IMSI
|
||||
)
|
||||
data, sw = self._scc.send_apdu_checksw("0099000033" + par)
|
||||
data, sw = self._scc._tp.send_apdu_checksw("0099000033" + par)
|
||||
|
||||
|
||||
class SysmoSIMgr2(SimCard):
|
||||
@@ -825,7 +826,7 @@ class SysmoSIMgr2(SimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b7d9400005555530a7486930b247c4d5468":
|
||||
if scc.get_atr() == toBytes("3B 7D 94 00 00 55 55 53 0A 74 86 93 0B 24 7C 4D 54 68"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -850,7 +851,7 @@ class SysmoSIMgr2(SimCard):
|
||||
pin = h2b("4444444444444444")
|
||||
|
||||
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)
|
||||
|
||||
@@ -903,7 +904,7 @@ class SysmoUSIMSJS1(UsimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b9f96801fc78031a073be21136743200718000001a5":
|
||||
if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 43 20 07 18 00 00 01 A5"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1031,7 +1032,7 @@ class FairwavesSIM(UsimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b9f96801fc78031a073be21136744220610000001a9":
|
||||
if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 44 22 06 10 00 00 01 A9"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1165,7 +1166,7 @@ class OpenCellsSim(SimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b9f95801fc38031e073fe21135786810286984418a8":
|
||||
if scc.get_atr() == toBytes("3B 9F 95 80 1F C3 80 31 E0 73 FE 21 13 57 86 81 02 86 98 44 18 A8"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1214,7 +1215,7 @@ class WavemobileSim(UsimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b9f95801fc78031e073f62113674d4516004301008f":
|
||||
if scc.get_atr() == toBytes("3B 9F 95 80 1F C7 80 31 E0 73 F6 21 13 67 4D 45 16 00 43 01 00 8F"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1304,18 +1305,18 @@ class SysmoISIMSJA2(UsimCard, IsimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Try card model #1
|
||||
atr = "3b9f96801f878031e073fe211b674a4c753034054ba9"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
|
||||
# Try card model #2
|
||||
atr = "3b9f96801f878031e073fe211b674a4c7531330251b2"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
|
||||
# Try card model #3
|
||||
atr = "3b9f96801f878031e073fe211b674a4c5275310451d5"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1553,16 +1554,16 @@ class SysmoISIMSJA5(SysmoISIMSJA2):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Try card model #1 (9FJ)
|
||||
atr = "3b9f96801f878031e073fe211b674a357530350251cc"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
# Try card model #2 (SLM17)
|
||||
atr = "3b9f96801f878031e073fe211b674a357530350265f8"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
# Try card model #3 (9FV)
|
||||
atr = "3b9f96801f878031e073fe211b674a357530350259c4"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1591,7 +1592,7 @@ class GialerSim(UsimCard):
|
||||
def autodetect(cls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == '3b9f95801fc78031a073b6a10067cf3215ca9cd70920':
|
||||
if scc.get_atr() == toBytes('3B 9F 95 80 1F C7 80 31 A0 73 B6 A1 00 67 CF 32 15 CA 9C D7 09 20'):
|
||||
return cls(scc)
|
||||
except:
|
||||
return None
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from typing import Optional, Tuple
|
||||
from pySim.utils import Hexstr, rpad, enc_plmn, h2i, i2s, s2h
|
||||
from pySim.utils import dec_xplmn_w_act, dec_xplmn, dec_mcc_from_plmn, dec_mnc_from_plmn
|
||||
from osmocom.utils import swap_nibbles, h2b, b2h
|
||||
|
||||
def hexstr_to_Nbytearr(s, nbytes):
|
||||
return [s[i:i+(nbytes*2)] for i in range(0, len(s), (nbytes*2))]
|
||||
@@ -332,82 +330,3 @@ def enc_addr_tlv(addr, addr_type='00'):
|
||||
s += '80' + ('%02x' % ((len(ipv4_str)//2)+2)) + '01' + 'ff' + ipv4_str
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def dec_msisdn(ef_msisdn: Hexstr) -> Optional[Tuple[int, int, Optional[str]]]:
|
||||
"""
|
||||
Decode MSISDN from EF.MSISDN or EF.ADN (same structure).
|
||||
See 3GPP TS 31.102, section 4.2.26 and 4.4.2.3.
|
||||
"""
|
||||
|
||||
# Convert from str to (kind of) 'bytes'
|
||||
ef_msisdn = h2b(ef_msisdn)
|
||||
|
||||
# Make sure mandatory fields are present
|
||||
if len(ef_msisdn) < 14:
|
||||
raise ValueError("EF.MSISDN is too short")
|
||||
|
||||
# Skip optional Alpha Identifier
|
||||
xlen = len(ef_msisdn) - 14
|
||||
msisdn_lhv = ef_msisdn[xlen:]
|
||||
|
||||
# Parse the length (in bytes) of the BCD encoded number
|
||||
bcd_len = msisdn_lhv[0]
|
||||
# BCD length = length of dial num (max. 10 bytes) + 1 byte ToN and NPI
|
||||
if bcd_len == 0xff:
|
||||
return None
|
||||
elif bcd_len > 11 or bcd_len < 1:
|
||||
raise ValueError(
|
||||
"Length of MSISDN (%d bytes) is out of range" % bcd_len)
|
||||
|
||||
# Parse ToN / NPI
|
||||
ton = (msisdn_lhv[1] >> 4) & 0x07
|
||||
npi = msisdn_lhv[1] & 0x0f
|
||||
bcd_len -= 1
|
||||
|
||||
# No MSISDN?
|
||||
if not bcd_len:
|
||||
return (npi, ton, None)
|
||||
|
||||
msisdn = swap_nibbles(b2h(msisdn_lhv[2:][:bcd_len])).rstrip('f')
|
||||
# International number 10.5.118/3GPP TS 24.008
|
||||
if ton == 0x01:
|
||||
msisdn = '+' + msisdn
|
||||
|
||||
return (npi, ton, msisdn)
|
||||
|
||||
|
||||
def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
|
||||
"""
|
||||
Encode MSISDN as LHV so it can be stored to EF.MSISDN.
|
||||
See 3GPP TS 31.102, section 4.2.26 and 4.4.2.3. (The result
|
||||
will not contain the optional Alpha Identifier at the beginning.)
|
||||
|
||||
Default NPI / ToN values:
|
||||
- NPI: ISDN / telephony numbering plan (E.164 / E.163),
|
||||
- ToN: network specific or international number (if starts with '+').
|
||||
"""
|
||||
|
||||
# If no MSISDN is supplied then encode the file contents as all "ff"
|
||||
if msisdn in ["", "+"]:
|
||||
return "ff" * 14
|
||||
|
||||
# Leading '+' indicates International Number
|
||||
if msisdn[0] == '+':
|
||||
msisdn = msisdn[1:]
|
||||
ton = 0x01
|
||||
|
||||
# An MSISDN must not exceed 20 digits
|
||||
if len(msisdn) > 20:
|
||||
raise ValueError("msisdn must not be longer than 20 digits")
|
||||
|
||||
# Append 'f' padding if number of digits is odd
|
||||
if len(msisdn) % 2 > 0:
|
||||
msisdn += 'f'
|
||||
|
||||
# BCD length also includes NPI/ToN header
|
||||
bcd_len = len(msisdn) // 2 + 1
|
||||
npi_ton = (npi & 0x0f) | ((ton & 0x07) << 4) | 0x80
|
||||
bcd = rpad(swap_nibbles(msisdn), 10 * 2) # pad to 10 octets
|
||||
|
||||
return ('%02x' % bcd_len) + ('%02x' % npi_ton) + bcd + ("ff" * 2)
|
||||
|
||||
120
pySim/ota.py
120
pySim/ota.py
@@ -1,6 +1,6 @@
|
||||
"""Code related to SIM/UICC OTA according to TS 102 225 + TS 31.115."""
|
||||
|
||||
# (C) 2021-2024 by Harald Welte <laforge@osmocom.org>
|
||||
# (C) 2021-2023 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -15,16 +15,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pySim.construct import *
|
||||
from pySim.utils import b2h
|
||||
from pySim.sms import UserDataHeader
|
||||
from construct import *
|
||||
import zlib
|
||||
import abc
|
||||
import struct
|
||||
from typing import Optional, Tuple
|
||||
from construct import Enum, Int8ub, Int16ub, Struct, BitsInteger, BitStruct
|
||||
from construct import Flag, Padding, Switch, this, PrefixedArray, GreedyRange
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import b2h
|
||||
|
||||
from pySim.sms import UserDataHeader
|
||||
from typing import Optional
|
||||
|
||||
# ETS TS 102 225 gives the general command structure and the dialects for CAT_TP, TCP/IP and HTTPS
|
||||
# 3GPP TS 31.115 gives the dialects for SMS-PP, SMS-CB, USSD and HTTP
|
||||
@@ -99,17 +97,6 @@ SmsCommandPacket = Struct('cmd_pkt_len'/Int16ub,
|
||||
'tar'/Bytes(3),
|
||||
'secured_data'/GreedyBytes)
|
||||
|
||||
# TS 102 226 Section 8.2.1.3.2.1
|
||||
SimFileAccessAndToolkitAppSpecParams = Struct('access_domain'/Prefixed(Int8ub, GreedyBytes),
|
||||
'prio_level_of_tk_app_inst'/Int8ub,
|
||||
'max_num_of_timers'/Int8ub,
|
||||
'max_text_length_for_menu_entry'/Int8ub,
|
||||
'menu_entries'/PrefixedArray(Int8ub, Struct('id'/Int8ub,
|
||||
'pos'/Int8ub)),
|
||||
'max_num_of_channels'/Int8ub,
|
||||
'msl'/Prefixed(Int8ub, GreedyBytes),
|
||||
'tar_values'/Prefixed(Int8ub, GreedyRange(Bytes(3))))
|
||||
|
||||
class OtaKeyset:
|
||||
"""The OTA related data (key material, counter) to be used in encrypt/decrypt."""
|
||||
def __init__(self, algo_crypt: str, kic_idx: int, kic: bytes,
|
||||
@@ -125,12 +112,12 @@ class OtaKeyset:
|
||||
@property
|
||||
def auth(self):
|
||||
"""Return an instance of the matching OtaAlgoAuth."""
|
||||
return OtaAlgoAuth.from_keyset(self)
|
||||
return OtaAlgoAuth.fromKeyset(self)
|
||||
|
||||
@property
|
||||
def crypt(self):
|
||||
"""Return an instance of the matching OtaAlgoCrypt."""
|
||||
return OtaAlgoCrypt.from_keyset(self)
|
||||
return OtaAlgoCrypt.fromKeyset(self)
|
||||
|
||||
class OtaCheckError(Exception):
|
||||
pass
|
||||
@@ -141,24 +128,26 @@ class OtaDialect(abc.ABC):
|
||||
def _compute_sig_len(self, spi:SPI):
|
||||
if spi['rc_cc_ds'] == 'no_rc_cc_ds':
|
||||
return 0
|
||||
if spi['rc_cc_ds'] == 'rc': # CRC-32
|
||||
elif spi['rc_cc_ds'] == 'rc': # CRC-32
|
||||
return 4
|
||||
if spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
|
||||
elif spi['rc_cc_ds'] == 'cc': # Cryptographic Checksum (CC)
|
||||
# TODO: this is not entirely correct, as in AES case it could be 4 or 8
|
||||
return 8
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
else:
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
|
||||
@abc.abstractmethod
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, apdu: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
|
||||
def decode_resp(self, otak: OtaKeyset, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
|
||||
"""Decode a response into a response packet and, if indicted (by a
|
||||
response status of `"por_ok"`) a decoded response.
|
||||
|
||||
The response packet's common characteristics are not fully determined,
|
||||
and (so far) completely proprietary per dialect."""
|
||||
pass
|
||||
|
||||
|
||||
from Cryptodome.Cipher import DES, DES3, AES
|
||||
@@ -201,7 +190,7 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
|
||||
def encrypt(self, data:bytes) -> bytes:
|
||||
"""Encrypt given input bytes using the key material given in constructor."""
|
||||
padded_data = self.pad_to_blocksize(data)
|
||||
return self._encrypt(padded_data)
|
||||
return self._encrypt(data)
|
||||
|
||||
def decrypt(self, data:bytes) -> bytes:
|
||||
"""Decrypt given input bytes using the key material given in constructor."""
|
||||
@@ -210,13 +199,15 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def _encrypt(self, data:bytes) -> bytes:
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _decrypt(self, data:bytes) -> bytes:
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoCrypt':
|
||||
def fromKeyset(cls, otak: OtaKeyset) -> 'OtaAlgoCrypt':
|
||||
"""Resolve the class for the encryption algorithm of otak and instantiate it."""
|
||||
for subc in cls.__subclasses__():
|
||||
if subc.enum_name == otak.algo_crypt:
|
||||
@@ -248,7 +239,7 @@ class OtaAlgoAuth(OtaAlgo, abc.ABC):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_keyset(cls, otak: OtaKeyset) -> 'OtaAlgoAuth':
|
||||
def fromKeyset(cls, otak: OtaKeyset) -> 'OtaAlgoAuth':
|
||||
"""Resolve the class for the authentication algorithm of otak and instantiate it."""
|
||||
for subc in cls.__subclasses__():
|
||||
if subc.enum_name == otak.algo_auth:
|
||||
@@ -302,7 +293,7 @@ class OtaAlgoAuthDES3(OtaAlgoAuth):
|
||||
class OtaAlgoCryptAES(OtaAlgoCrypt):
|
||||
name = 'AES'
|
||||
enum_name = 'aes_cbc'
|
||||
blocksize = 16
|
||||
blocksize = 16 # TODO: is this needed?
|
||||
def _encrypt(self, data:bytes) -> bytes:
|
||||
cipher = AES.new(self.otak.kic, AES.MODE_CBC, self.iv)
|
||||
return cipher.encrypt(data)
|
||||
@@ -333,7 +324,6 @@ class OtaDialectSms(OtaDialect):
|
||||
'response_status'/ResponseStatus,
|
||||
'cc_rc'/Bytes(this.rhl-10),
|
||||
'secured_data'/GreedyBytes)
|
||||
hdr_construct = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
|
||||
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
||||
# length of signature in octets
|
||||
@@ -344,7 +334,6 @@ class OtaDialectSms(OtaDialect):
|
||||
len_cipher = 6 + len_sig + len(apdu)
|
||||
padding = otak.crypt._get_padding(len_cipher, otak.crypt.blocksize)
|
||||
pad_cnt = len(padding)
|
||||
apdu = bytes(apdu) # make a copy so we don't modify the input data
|
||||
apdu += padding
|
||||
|
||||
kic = {'key': otak.kic_idx, 'algo': otak.algo_crypt}
|
||||
@@ -355,21 +344,22 @@ class OtaDialectSms(OtaDialect):
|
||||
chl = 13 + len_sig
|
||||
|
||||
# CHL + SPI (+ KIC + KID)
|
||||
part_head = self.hdr_construct.build({'chl': chl, 'spi':spi, 'kic':kic, 'kid':kid, 'tar':tar})
|
||||
print("part_head: %s" % b2h(part_head))
|
||||
c = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
|
||||
part_head = c.build({'chl': chl, 'spi':spi, 'kic':kic, 'kid':kid, 'tar':tar})
|
||||
#print("part_head: %s" % b2h(part_head))
|
||||
|
||||
# CNTR + PCNTR (CNTR not used)
|
||||
part_cnt = otak.cntr.to_bytes(5, 'big') + pad_cnt.to_bytes(1, 'big')
|
||||
print("part_cnt: %s" % b2h(part_cnt))
|
||||
#print("part_cnt: %s" % b2h(part_cnt))
|
||||
|
||||
envelope_data = part_head + part_cnt + apdu
|
||||
print("envelope_data: %s" % b2h(envelope_data))
|
||||
#print("envelope_data: %s" % b2h(envelope_data))
|
||||
|
||||
# 2-byte CPL. CPL is part of RC/CC/CPI to end of secured data, including any padding for ciphering
|
||||
# CPL from and including CPI to end of secured data, including any padding for ciphering
|
||||
cpl = len(envelope_data) + len_sig
|
||||
envelope_data = cpl.to_bytes(2, 'big') + envelope_data
|
||||
print("envelope_data with cpl: %s" % b2h(envelope_data))
|
||||
#print("envelope_data with cpl: %s" % b2h(envelope_data))
|
||||
|
||||
if spi['rc_cc_ds'] == 'cc':
|
||||
cc = otak.auth.sign(envelope_data)
|
||||
@@ -383,7 +373,7 @@ class OtaDialectSms(OtaDialect):
|
||||
else:
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
|
||||
print("envelope_data with sig: %s" % b2h(envelope_data))
|
||||
#print("envelope_data with sig: %s" % b2h(envelope_data))
|
||||
|
||||
# encrypt as needed
|
||||
if spi['ciphering']: # ciphering is requested
|
||||
@@ -395,57 +385,10 @@ class OtaDialectSms(OtaDialect):
|
||||
else:
|
||||
envelope_data = part_head + 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')
|
||||
#print("envelope_data: %s" % b2h(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"]):
|
||||
if isinstance(data, str):
|
||||
data = h2b(data)
|
||||
@@ -456,7 +399,7 @@ class OtaDialectSms(OtaDialect):
|
||||
# POR with CC+CIPH: 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
|
||||
if data[0] != 0x02:
|
||||
raise ValueError('Unexpected UDL=0x%02x' % data[0])
|
||||
udhd, remainder = UserDataHeader.from_bytes(data)
|
||||
udhd, remainder = UserDataHeader.fromBytes(data)
|
||||
if not udhd.has_ie(0x71):
|
||||
raise ValueError('RPI 0x71 not found in UDH')
|
||||
rph_rhl_tar = remainder[:6] # RPH+RHL+TAR; not ciphered
|
||||
@@ -492,9 +435,8 @@ class OtaDialectSms(OtaDialect):
|
||||
else:
|
||||
raise OtaCheckError('Unknown por_rc_cc_ds: %s' % spi['por_rc_cc_ds'])
|
||||
|
||||
print(res)
|
||||
# TODO: ExpandedRemoteResponse according to TS 102 226 5.2.2
|
||||
if res.response_status == 'por_ok' and len(res['secured_data']):
|
||||
if res.response_status == 'por_ok':
|
||||
dec = CompactRemoteResp.parse(res['secured_data'])
|
||||
else:
|
||||
dec = None
|
||||
|
||||
@@ -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/>.
|
||||
#
|
||||
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.filesystem import CardApplication, interpret_sw
|
||||
from pySim.utils import all_subclasses
|
||||
import abc
|
||||
import operator
|
||||
from typing import List
|
||||
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.filesystem import CardApplication, interpret_sw
|
||||
from pySim.utils import all_subclasses
|
||||
|
||||
def _mf_select_test(scc: SimCardCommands,
|
||||
cla_byte: str, sel_ctrl: str,
|
||||
fids: List[str]) -> bool:
|
||||
cla_byte_bak = scc.cla_byte
|
||||
sel_ctrl_bak = scc.sel_ctrl
|
||||
scc.reset_card()
|
||||
|
||||
scc.cla_byte = cla_byte
|
||||
scc.sel_ctrl = sel_ctrl
|
||||
rc = True
|
||||
try:
|
||||
for fid in fids:
|
||||
scc.select_file(fid)
|
||||
except:
|
||||
rc = False
|
||||
|
||||
scc.reset_card()
|
||||
scc.cla_byte = cla_byte_bak
|
||||
scc.sel_ctrl = sel_ctrl_bak
|
||||
return rc
|
||||
|
||||
|
||||
def match_uicc(scc: SimCardCommands) -> bool:
|
||||
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
|
||||
card is considered a UICC card.
|
||||
"""
|
||||
return _mf_select_test(scc, "00", "0004", ["3f00"])
|
||||
|
||||
|
||||
def match_sim(scc: SimCardCommands) -> bool:
|
||||
""" Try to access MF via 2G APDUs (3GPP TS 11.11), if this works, the card
|
||||
is also a simcard. This will be the case for most UICC cards, but there may
|
||||
also be plain UICC cards without 2G support as well.
|
||||
"""
|
||||
return _mf_select_test(scc, "a0", "0000", ["3f00"])
|
||||
|
||||
|
||||
def match_ruim(scc: SimCardCommands) -> bool:
|
||||
""" Try to access MF/DF.CDMA via 2G APDUs (3GPP TS 11.11), if this works,
|
||||
the card is considered an R-UIM card for CDMA.
|
||||
"""
|
||||
return _mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
|
||||
|
||||
|
||||
class CardProfile:
|
||||
"""A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of
|
||||
@@ -95,39 +138,13 @@ class CardProfile:
|
||||
return data_hex
|
||||
|
||||
@staticmethod
|
||||
def _mf_select_test(scc: SimCardCommands,
|
||||
cla_byte: str, sel_ctrl: str,
|
||||
fids: List[str]) -> bool:
|
||||
"""Helper function used by some derived _try_match_card() methods."""
|
||||
scc.reset_card()
|
||||
|
||||
scc.cla_byte = cla_byte
|
||||
scc.sel_ctrl = sel_ctrl
|
||||
for fid in fids:
|
||||
scc.select_file(fid)
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
"""Try to see if the specific profile matches the card. This method is a
|
||||
def match_with_card(scc: SimCardCommands) -> bool:
|
||||
"""Check if the specific profile matches the card. This method is a
|
||||
placeholder that is overloaded by specific dirived classes. The method
|
||||
actively probes the card to make sure the profile class matches the
|
||||
physical card. This usually also means that the card is reset during
|
||||
the process, so this method must not be called at random times. It may
|
||||
only be called on startup. If there is no exception raised, we assume
|
||||
the card matches the profile.
|
||||
|
||||
Args:
|
||||
scc: SimCardCommands class
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def match_with_card(cls, scc: SimCardCommands) -> bool:
|
||||
"""Check if the specific profile matches the card. The method
|
||||
actively probes the card to make sure the profile class matches the
|
||||
physical card. This usually also means that the card is reset during
|
||||
the process, so this method must not be called at random times. It may
|
||||
only be called on startup.
|
||||
|
||||
Args:
|
||||
@@ -135,17 +152,7 @@ class CardProfile:
|
||||
Returns:
|
||||
match = True, no match = False
|
||||
"""
|
||||
sel_backup = scc.sel_ctrl
|
||||
cla_backup = scc.cla_byte
|
||||
try:
|
||||
cls._try_match_card(scc)
|
||||
return True
|
||||
except SwMatchError:
|
||||
return False
|
||||
finally:
|
||||
scc.sel_ctrl = sel_backup
|
||||
scc.cla_byte = cla_backup
|
||||
scc.reset_card()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def pick(scc: SimCardCommands):
|
||||
@@ -159,7 +166,7 @@ class CardProfile:
|
||||
return None
|
||||
|
||||
def add_addon(self, addon: 'CardProfileAddon'):
|
||||
assert addon not in self.addons
|
||||
assert(addon not in self.addons)
|
||||
# we don't install any additional files, as that is happening in the RuntimeState.
|
||||
self.addons.append(addon)
|
||||
|
||||
@@ -179,6 +186,7 @@ class CardProfileAddon(abc.ABC):
|
||||
self.desc = kw.get("desc", None)
|
||||
self.files_in_mf = kw.get("files_in_mf", [])
|
||||
self.shell_cmdsets = kw.get("shell_cmdsets", [])
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -186,3 +194,4 @@ class CardProfileAddon(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def probe(self, card: 'CardBase') -> bool:
|
||||
"""Probe a given card to determine whether or not this add-on is present/supported."""
|
||||
pass
|
||||
|
||||
166
pySim/runtime.py
166
pySim/runtime.py
@@ -18,9 +18,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional, Tuple
|
||||
from osmocom.utils import h2b, i2h, is_hex, Hexstr
|
||||
from osmocom.tlv import bertlv_parse_one
|
||||
|
||||
from pySim.utils import sw_match, h2b, i2h, is_hex, bertlv_parse_one, Hexstr
|
||||
from pySim.exceptions import *
|
||||
from pySim.filesystem import *
|
||||
|
||||
@@ -30,10 +29,11 @@ def lchan_nr_from_cla(cla: int) -> int:
|
||||
if cla >> 4 in [0x0, 0xA, 0x8]:
|
||||
# Table 10.3
|
||||
return cla & 0x03
|
||||
if cla & 0xD0 in [0x40, 0xC0]:
|
||||
elif cla & 0xD0 in [0x40, 0xC0]:
|
||||
# Table 10.4a
|
||||
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:
|
||||
"""Represent the runtime state of a session with a card."""
|
||||
@@ -50,10 +50,6 @@ class RuntimeState:
|
||||
self.lchan = {}
|
||||
# the basic logical channel always exists
|
||||
self.lchan[0] = RuntimeLchan(0, self)
|
||||
# this is a dict of card identities which different parts of the code might populate,
|
||||
# typically with something like ICCID, EID, ATR, ...
|
||||
self.identity = {}
|
||||
self.adm_verified = False
|
||||
|
||||
# make sure the class and selection control bytes, which are specified
|
||||
# by the card profile are used
|
||||
@@ -120,7 +116,7 @@ class RuntimeState:
|
||||
# no problem when we access the card object directly without caring
|
||||
# about updating other states. For normal selects at runtime, the
|
||||
# caller must use the lchan provided methods select or select_file!
|
||||
_data, sw = self.card.select_adf_by_aid(f.aid)
|
||||
data, sw = self.card.select_adf_by_aid(f.aid)
|
||||
self.selected_adf = f
|
||||
if sw == "9000":
|
||||
print(" %s: %s" % (f.name, f.aid))
|
||||
@@ -136,19 +132,13 @@ class RuntimeState:
|
||||
"""
|
||||
# delete all lchan != 0 (basic lchan)
|
||||
for lchan_nr in list(self.lchan.keys()):
|
||||
self.lchan[lchan_nr].scc.scp = None
|
||||
if lchan_nr == 0:
|
||||
continue
|
||||
del self.lchan[lchan_nr]
|
||||
self.adm_verified = False
|
||||
atr = self.card.reset()
|
||||
if cmd_app:
|
||||
cmd_app.lchan = self.lchan[0]
|
||||
atr = i2h(self.card.reset())
|
||||
# select MF to reset internal state and to verify card really works
|
||||
self.lchan[0].select('MF', cmd_app)
|
||||
self.lchan[0].selected_adf = None
|
||||
# store ATR as part of our card identies dict
|
||||
self.identity['ATR'] = atr
|
||||
return atr
|
||||
|
||||
def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
|
||||
@@ -213,18 +203,6 @@ class RuntimeLchan:
|
||||
def selected_file_num_of_rec(self) -> Optional[int]:
|
||||
return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
|
||||
|
||||
def selected_file_record_len(self) -> Optional[int]:
|
||||
return self.selected_file_fcp['file_descriptor'].get('record_len')
|
||||
|
||||
def selected_file_size(self) -> Optional[int]:
|
||||
return self.selected_file_fcp.get('file_size')
|
||||
|
||||
def selected_file_reserved_file_size(self) -> Optional[int]:
|
||||
return self.selected_file_fcp['proprietary_information'].get('reserved_file_size')
|
||||
|
||||
def selected_file_maximum_file_size(self) -> Optional[int]:
|
||||
return self.selected_file_fcp['proprietary_information'].get('maximum_file_size')
|
||||
|
||||
def get_cwd(self) -> CardDF:
|
||||
"""Obtain the current working directory.
|
||||
|
||||
@@ -249,42 +227,6 @@ class RuntimeLchan:
|
||||
node = node.parent
|
||||
return None
|
||||
|
||||
def get_file_by_name(self, name: str) -> CardFile:
|
||||
"""Obtain the file object from the file system tree by its name without actually selecting the file.
|
||||
|
||||
Returns:
|
||||
CardFile() instance or None"""
|
||||
|
||||
# handling of entire paths with multiple directories/elements
|
||||
if '/' in name:
|
||||
pathlist = name.split('/')
|
||||
# treat /DF.GSM/foo like MF/DF.GSM/foo
|
||||
if pathlist[0] == '':
|
||||
pathlist[0] = 'MF'
|
||||
else:
|
||||
pathlist = [name]
|
||||
|
||||
# start in the current working directory (we can still
|
||||
# select any ADF and the MF from here, so those will be
|
||||
# among the selectables).
|
||||
file = self.get_cwd()
|
||||
|
||||
for p in pathlist:
|
||||
# Look for the next file in the path list
|
||||
selectables = file.get_selectables()
|
||||
file = None
|
||||
for selectable in selectables:
|
||||
if selectable == p:
|
||||
file = selectables[selectable]
|
||||
break
|
||||
|
||||
# When we hit none, then the given path must be invalid
|
||||
if file is None:
|
||||
return None
|
||||
|
||||
# Return the file object found at the tip of the path
|
||||
return file
|
||||
|
||||
def interpret_sw(self, sw: str):
|
||||
"""Interpret a given status word relative to the currently selected application
|
||||
or the underlying card profile.
|
||||
@@ -313,8 +255,7 @@ class RuntimeLchan:
|
||||
raise ValueError(
|
||||
"Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
|
||||
|
||||
# unregister commands of old file
|
||||
self.unregister_cmds(cmd_app)
|
||||
self._select_pre(cmd_app)
|
||||
|
||||
try:
|
||||
# We access the card through the select_file method of the scc object.
|
||||
@@ -323,20 +264,20 @@ class RuntimeLchan:
|
||||
# run time. In case the file does not exist on the card, we just abort.
|
||||
# The state on the card (selected file/application) wont't be changed,
|
||||
# so we do not have to update any state in that case.
|
||||
(data, _sw) = self.scc.select_file(fid)
|
||||
(data, sw) = self.scc.select_file(fid)
|
||||
except SwMatchError as swm:
|
||||
self._select_post(cmd_app)
|
||||
k = self.interpret_sw(swm.sw_actual)
|
||||
if not k:
|
||||
raise swm
|
||||
raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1])) from swm
|
||||
raise(swm)
|
||||
raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
|
||||
|
||||
select_resp = self.selected_file.decode_select_response(data)
|
||||
if select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df':
|
||||
if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
|
||||
f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
|
||||
desc="dedicated file, manually added at runtime")
|
||||
else:
|
||||
if select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent':
|
||||
if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
|
||||
f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
|
||||
desc="elementary file, manually added at runtime")
|
||||
else:
|
||||
@@ -347,6 +288,12 @@ class RuntimeLchan:
|
||||
|
||||
self._select_post(cmd_app, f, data)
|
||||
|
||||
def _select_pre(self, cmd_app):
|
||||
# unregister commands of old file
|
||||
if cmd_app and self.selected_file.shell_commands:
|
||||
for c in self.selected_file.shell_commands:
|
||||
cmd_app.unregister_command_set(c)
|
||||
|
||||
def _select_post(self, cmd_app, file:Optional[CardFile] = None, select_resp_data = None):
|
||||
# we store some reference data (see above) about the currently selected file.
|
||||
# This data must be updated after every select.
|
||||
@@ -357,12 +304,11 @@ class RuntimeLchan:
|
||||
if select_resp_data:
|
||||
self.selected_file_fcp_hex = select_resp_data
|
||||
self.selected_file_fcp = self.selected_file.decode_select_response(select_resp_data)
|
||||
else:
|
||||
self.selected_file_fcp_hex = None
|
||||
self.selected_file_fcp = None
|
||||
|
||||
# register commands of new file
|
||||
self.register_cmds(cmd_app)
|
||||
if cmd_app and self.selected_file.shell_commands:
|
||||
for c in self.selected_file.shell_commands:
|
||||
cmd_app.register_command_set(c)
|
||||
|
||||
def select_file(self, file: CardFile, cmd_app=None):
|
||||
"""Select a file (EF, DF, ADF, MF, ...).
|
||||
@@ -371,31 +317,11 @@ class RuntimeLchan:
|
||||
file : CardFile [or derived class] instance
|
||||
cmd_app : Command Application State (for unregistering old file commands)
|
||||
"""
|
||||
|
||||
if not isinstance(file, CardADF) and self.selected_adf and self.selected_adf.has_fs == False:
|
||||
# Not every application that may be present on a GlobalPlatform card will support the SELECT
|
||||
# command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
|
||||
# SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
|
||||
# Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
|
||||
# "select by name" method, which means we can only select an application and not a file.
|
||||
# The consequence of this is that we may get trapped in an application that does not have
|
||||
# ISIM/USIM like file system support and the only way to leave that application is to select
|
||||
# an ISIM/USIM application in order to get the file system access back.
|
||||
#
|
||||
# To automate this escape-route we will first select an arbitrary ADF that has file system support first
|
||||
# and then continue normally.
|
||||
for selectable in self.rs.mf.get_selectables().items():
|
||||
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
|
||||
self.select(selectable[1].name, cmd_app)
|
||||
break
|
||||
|
||||
# we need to find a path from our self.selected_file to the destination
|
||||
inter_path = self.selected_file.build_select_path_to(file)
|
||||
if not inter_path:
|
||||
raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
|
||||
|
||||
# unregister commands of old file
|
||||
self.unregister_cmds(cmd_app)
|
||||
self._select_pre(cmd_app)
|
||||
|
||||
# be sure the variables that we pass to _select_post contain valid values.
|
||||
selected_file = self.selected_file
|
||||
@@ -411,13 +337,13 @@ class RuntimeLchan:
|
||||
# card directly since this would lead into an incoherence of the
|
||||
# card state and the state of the lchan.
|
||||
if isinstance(f, CardADF):
|
||||
(data, _sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
|
||||
(data, sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
|
||||
else:
|
||||
(data, _sw) = self.scc.select_file(f.fid)
|
||||
(data, sw) = self.scc.select_file(f.fid)
|
||||
selected_file = f
|
||||
except SwMatchError as swm:
|
||||
self._select_post(cmd_app, selected_file, data)
|
||||
raise swm
|
||||
raise(swm)
|
||||
|
||||
self._select_post(cmd_app, f, data)
|
||||
|
||||
@@ -465,11 +391,10 @@ class RuntimeLchan:
|
||||
|
||||
def status(self):
|
||||
"""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)
|
||||
|
||||
def get_file_for_filename(self, name: str):
|
||||
"""Get the related CardFile object for a specified filename."""
|
||||
def get_file_for_selectable(self, name: str):
|
||||
sels = self.selected_file.get_selectables()
|
||||
return sels[name]
|
||||
|
||||
@@ -490,8 +415,7 @@ class RuntimeLchan:
|
||||
binary data read from the file
|
||||
"""
|
||||
if not isinstance(self.selected_file, TransparentEF):
|
||||
raise TypeError("Only works with TransparentEF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
raise TypeError("Only works with TransparentEF")
|
||||
return self.scc.read_binary(self.selected_file.fid, length, offset)
|
||||
|
||||
def read_binary_dec(self) -> Tuple[dict, str]:
|
||||
@@ -515,8 +439,7 @@ class RuntimeLchan:
|
||||
offset : Offset into the file from which to write 'data_hex'
|
||||
"""
|
||||
if not isinstance(self.selected_file, TransparentEF):
|
||||
raise TypeError("Only works with TransparentEF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
raise TypeError("Only works with TransparentEF")
|
||||
return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
|
||||
|
||||
def update_binary_dec(self, data: dict):
|
||||
@@ -526,7 +449,7 @@ class RuntimeLchan:
|
||||
Args:
|
||||
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)
|
||||
|
||||
def read_record(self, rec_nr: int = 0):
|
||||
@@ -538,8 +461,7 @@ class RuntimeLchan:
|
||||
hex string of binary data contained in record
|
||||
"""
|
||||
if not isinstance(self.selected_file, LinFixedEF):
|
||||
raise TypeError("Only works with Linear Fixed EF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
raise TypeError("Only works with Linear Fixed EF")
|
||||
# returns a string of hex nibbles
|
||||
return self.scc.read_record(self.selected_file.fid, rec_nr)
|
||||
|
||||
@@ -562,8 +484,7 @@ class RuntimeLchan:
|
||||
data_hex : Hex string binary data to be written
|
||||
"""
|
||||
if not isinstance(self.selected_file, LinFixedEF):
|
||||
raise TypeError("Only works with Linear Fixed EF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
raise TypeError("Only works with Linear Fixed EF")
|
||||
return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
|
||||
conserve=self.rs.conserve_write,
|
||||
leftpad=self.selected_file.leftpad)
|
||||
@@ -576,7 +497,7 @@ class RuntimeLchan:
|
||||
rec_nr : Record number to read
|
||||
data_hex : Abstract data to be written
|
||||
"""
|
||||
data_hex = self.selected_file.encode_record_hex(data, rec_nr, self.selected_file_record_len())
|
||||
data_hex = self.selected_file.encode_record_hex(data, rec_nr)
|
||||
return self.update_record(rec_nr, data_hex)
|
||||
|
||||
def retrieve_data(self, tag: int = 0):
|
||||
@@ -599,10 +520,9 @@ class RuntimeLchan:
|
||||
list of integer tags contained in EF
|
||||
"""
|
||||
if not isinstance(self.selected_file, BerTlvEF):
|
||||
raise TypeError("Only works with BER-TLV EF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
data, _sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
|
||||
_tag, _length, value, _remainder = bertlv_parse_one(h2b(data))
|
||||
raise TypeError("Only works with BER-TLV EF")
|
||||
data, sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c)
|
||||
tag, length, value, remainder = bertlv_parse_one(h2b(data))
|
||||
return list(value)
|
||||
|
||||
def set_data(self, tag: int, data_hex: str):
|
||||
@@ -613,18 +533,14 @@ class RuntimeLchan:
|
||||
data_hex : Hex string binary data to be written (value portion)
|
||||
"""
|
||||
if not isinstance(self.selected_file, BerTlvEF):
|
||||
raise TypeError("Only works with BER-TLV EF, but %s is %s" % (self.selected_file,
|
||||
self.selected_file.__class__.__mro__))
|
||||
raise TypeError("Only works with BER-TLV EF")
|
||||
return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
|
||||
|
||||
def register_cmds(self, cmd_app=None):
|
||||
"""Register command set that is associated with the currently selected file"""
|
||||
if cmd_app and self.selected_file.shell_commands:
|
||||
for c in self.selected_file.shell_commands:
|
||||
cmd_app.register_command_set(c)
|
||||
|
||||
def unregister_cmds(self, cmd_app=None):
|
||||
"""Unregister command set that is associated with the currently selected file"""
|
||||
"""Unregister all file specific commands."""
|
||||
if cmd_app and self.selected_file.shell_commands:
|
||||
for c in self.selected_file.shell_commands:
|
||||
cmd_app.unregister_command_set(c)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Generic code related to Secure Channel processing
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
from osmocom.utils import b2h, h2b, Hexstr
|
||||
|
||||
from pySim.utils import ResTuple
|
||||
|
||||
class SecureChannel(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def wrap_cmd_apdu(self, apdu: bytes) -> bytes:
|
||||
"""Wrap Command APDU according to specific Secure Channel Protocol."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
|
||||
"""UnWrap Response-APDU according to specific Secure Channel Protocol."""
|
||||
pass
|
||||
|
||||
def send_apdu_wrapper(self, send_fn: callable, pdu: Hexstr, *args, **kwargs) -> ResTuple:
|
||||
"""Wrapper function to wrap command APDU and unwrap repsonse APDU around send_apdu callable."""
|
||||
pdu_wrapped = b2h(self.wrap_cmd_apdu(h2b(pdu)))
|
||||
res, sw = send_fn(pdu_wrapped, *args, **kwargs)
|
||||
res_unwrapped = b2h(self.unwrap_rsp_apdu(h2b(sw), h2b(res)))
|
||||
return res_unwrapped, sw
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user