mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-10-04 07:26:05 +03:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4364309ec7 | |||
| b19fbc7a07 | |||
| 9a7d83da3d | |||
| 925573f4d7 | |||
| 9d08268bc4 |
@@ -1,3 +1,2 @@
|
||||
--exclude ^pySim/esim/asn1/.*\.asn$
|
||||
--exclude ^smdpp-data/.*$
|
||||
--exclude ^contrib/rcp/usage_example/certs/.*\.pem$
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Philipp Maier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU 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 os
|
||||
import websockets
|
||||
import asyncio
|
||||
import argparse
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from pySim.log import PySimLogger
|
||||
from rcp_utils import CltConnHdlr, backtrace, pytype_to_type, load_ca_cert, load_json_schema, JsonValidator
|
||||
from pySim.transport import init_reader, argparse_add_reader_args, LinkBase
|
||||
from packaging.version import Version
|
||||
|
||||
SERVER_TIMEOUT = 10
|
||||
|
||||
# The RCP Client software version shall be incremented when there are changes to the RCP Client (this module) or changes
|
||||
# to other related modules, which affect the RCP Client. The RCP Client software version is also disclosed towards the
|
||||
# RCP Server.
|
||||
RCPC_VERSION_SOFTWARE = "1.0.0"
|
||||
|
||||
# The RCP Client protocol version refers to the protocol spoken between RCP Client and RCP Server. The protocol version
|
||||
# shall be incremented when there are changes to the protocol (JSON Schema and/or application logic, see also
|
||||
# RCPC_VERSION_PROTOCOL in rcp_server.py).
|
||||
RCPC_VERSION_PROTOCOL = "1.0.0"
|
||||
|
||||
log = PySimLogger.get(Path(__file__).stem)
|
||||
option_parser = argparse.ArgumentParser(description='RCP Client',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
argparse_add_reader_args(option_parser)
|
||||
option_parser.add_argument("--verbose", help="Enable verbose logging",
|
||||
action='store_true', default=False)
|
||||
option_parser.add_argument("--uri", help="URI of the RCP-Server")
|
||||
option_parser.add_argument("--ca-cert", help="SSL/TLS CA-Certificate of the RCP-Server")
|
||||
|
||||
class RcpcCltConnHdlr(CltConnHdlr):
|
||||
def __init__(self, sl, *args, **kwargs):
|
||||
self.sl = sl
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def check_version(self):
|
||||
"""
|
||||
Send the Protocol and Software version of this RCP Client to the RCP Server. The RCP Server will then check
|
||||
if this client is (still) compatible. If an incompatibility is detected, the connection will be closed.
|
||||
"""
|
||||
log.info("Checking version ...")
|
||||
tx_json = {'rcpc_version': {'software' : RCPC_VERSION_SOFTWARE,
|
||||
'protocol' : RCPC_VERSION_PROTOCOL}}
|
||||
log.info("RCP Client version: software=%s, protocol=%s",
|
||||
RCPC_VERSION_SOFTWARE, RCPC_VERSION_PROTOCOL)
|
||||
rx_json = await self._transact(tx_json)
|
||||
rcps_version_software = Version(rx_json['rcpc_version']['software'])
|
||||
rcps_version_protocol = Version(rx_json['rcpc_version']['protocol'])
|
||||
rcps_version_info = str(rx_json['rcpc_version'].get('info'))
|
||||
if rcps_version_info:
|
||||
log.info("RCP Server version: software=%s, protocol=%s",
|
||||
rcps_version_software, rcps_version_protocol)
|
||||
else:
|
||||
log.info("RCP Server version: software=%s, protocol=%s, %s",
|
||||
rcps_version_software, rcps_version_protocol, rcps_version_info)
|
||||
|
||||
async def describe(self, suitable_for:dict) -> list:
|
||||
log.info("Requesting module descriptions from RCP Server ...")
|
||||
tx_json = {'rcpc_hello': {'suitable_for' : suitable_for}}
|
||||
rx_json = await self._transact(tx_json)
|
||||
module_descr = rx_json['rcpc_welcome']['module_descr']
|
||||
if not module_descr:
|
||||
raise ValueError("No RCP module available for this card")
|
||||
return module_descr
|
||||
|
||||
async def run(self, cmd:str, cmd_argv) -> int:
|
||||
log.info("Executing command with RCP Server ...")
|
||||
tx_json = {'rcpc_command': {'cmd' : cmd, 'cmd_argv' : cmd_argv}}
|
||||
while(True):
|
||||
rx_json = await self._transact(tx_json)
|
||||
tx_json = None
|
||||
if 'rcpc_instr' in rx_json:
|
||||
rcpc_instr = rx_json['rcpc_instr']
|
||||
if 'c_apdu' in rcpc_instr:
|
||||
c_apdu = rx_json['rcpc_instr']['c_apdu']
|
||||
data, sw = sl.send_apdu(c_apdu)
|
||||
tx_json = {'rcpc_result': {'r_apdu' : {'data': data.upper(), 'sw': sw.upper()}}}
|
||||
elif 'reset' in rcpc_instr:
|
||||
sl.reset_card()
|
||||
atr = sl.get_atr()
|
||||
tx_json = {'rcpc_result': {'atr' : atr.upper()}}
|
||||
elif 'print' in rcpc_instr:
|
||||
log.info(str(self) + " -- %s", rx_json['rcpc_instr']['print'])
|
||||
tx_json = {'rcpc_result': {'empty' : None}}
|
||||
elif 'rcpc_goodbye' in rx_json:
|
||||
rc = rx_json['rcpc_goodbye']
|
||||
log.info("Command execution done, rc: %d", rc)
|
||||
return rc
|
||||
|
||||
def check_if_user_needs_basic_help(argv):
|
||||
"""
|
||||
The '--uri' argument is the minimum requirement to connect to the RCP Server to retrieve the information about the
|
||||
dynamic commandline arguments. In case this argument is missing while '--help' or '-h' arguments are present. Then
|
||||
we will fall back to display only a basic help that contains only the static commandline arguments (see above).
|
||||
"""
|
||||
|
||||
if '--help' in argv or '-h' in argv:
|
||||
if '--uri' not in argv:
|
||||
option_parser.parse_args()
|
||||
sys.exit(1)
|
||||
|
||||
def parse_known_arguemnts(argv):
|
||||
"""
|
||||
Parse the commandline arguments we know so far. Ignore unknown arguments and filter out '--help' and '-h'
|
||||
arguments, in case those are present.
|
||||
"""
|
||||
|
||||
argv_filtered = deepcopy(argv)
|
||||
if '--help' in argv_filtered:
|
||||
argv_filtered.remove('--help')
|
||||
if '-h' in argv_filtered:
|
||||
argv_filtered.remove('-h')
|
||||
opts, unknown = option_parser.parse_known_args(argv_filtered)
|
||||
return opts
|
||||
|
||||
async def run_rcp_session(opts, sl, ssl_context) -> int:
|
||||
"""
|
||||
Connect to the RCP Server, retrieve the module description, use the module description to complete the commandline
|
||||
argument parser, execute the command that the user has selected.
|
||||
"""
|
||||
|
||||
# Request ATR from card
|
||||
card_atr = sl.get_atr().upper()
|
||||
log.info("Detected Card with ATR: %s" % card_atr)
|
||||
|
||||
# Connect to RCP server
|
||||
log.info("RCP Server URI: %s" % opts.uri)
|
||||
async with websockets.connect(opts.uri, ssl=ssl_context) as websocket:
|
||||
rcpc_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpc_to_rcps_schema.json"))
|
||||
rcps_to_rcpc_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpc_schema.json"))
|
||||
json_validator = JsonValidator(rcps_to_rcpc_schema, rcpc_to_rcps_schema)
|
||||
client = RcpcCltConnHdlr(sl, websocket, SERVER_TIMEOUT, json_validator)
|
||||
|
||||
# Check software and protocol version
|
||||
await client.check_version()
|
||||
|
||||
# Retrieve module description
|
||||
module_descrs = await client.describe({"atr" : card_atr})
|
||||
|
||||
# Complete the commandline parser and set up a dict that we can use as filter
|
||||
# TODO: Maybe it makes sense to integrate this as a method into the RcpcCltConnHdlr class?
|
||||
option_subparsers = option_parser.add_subparsers(dest='command', help="RCP command to use", required=True)
|
||||
sys_argv_filter = {}
|
||||
for module_descr in module_descrs:
|
||||
cmd_descr = module_descr['cmd_descr']
|
||||
for cmd in cmd_descr:
|
||||
command_name = module_descr['name'] + "_" + cmd['name']
|
||||
option_parser_cmd = option_subparsers.add_parser(command_name, help=cmd['help'])
|
||||
sys_argv_filter[command_name] = []
|
||||
for arg in cmd['args']:
|
||||
arg['spec'] = pytype_to_type(arg['spec'])
|
||||
option_parser_cmd.add_argument(arg['name'], **arg['spec'])
|
||||
sys_argv_filter[command_name].append(arg['name'])
|
||||
|
||||
# Re-Parse commandline options with the completed commandline parser. In case commandline help is
|
||||
# requested. The program is able to display the full help screen and exists.
|
||||
opts = option_parser.parse_args()
|
||||
|
||||
# Filter the relevant command arguments from sys.argv
|
||||
cmd_argv = []
|
||||
next_is_value=False
|
||||
for arg in sys.argv:
|
||||
if arg in sys_argv_filter[opts.command]:
|
||||
cmd_argv.append(arg)
|
||||
next_is_value=True
|
||||
elif next_is_value is True:
|
||||
next_is_value=False
|
||||
cmd_argv.append(arg)
|
||||
|
||||
# Run the command and close the connection
|
||||
rc = await client.run(opts.command, cmd_argv)
|
||||
await client.close()
|
||||
return rc
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# Setup logging
|
||||
PySimLogger.setup(print, {logging.WARN: "\033[33m", logging.DEBUG: "\033[90m"}, '--verbose' in sys.argv)
|
||||
|
||||
# Since parts of the commandline arguments are retrieved dynamically, we have to resolve a chicken-egg-problem.
|
||||
# We cannot call option_parser.parse_args() at the beginning, since we haven't received all information to
|
||||
# complete the option_parser yet. However in order to retrieve the arguments correctly we need to get the
|
||||
# URI and the parameters for the smartcard reader before we make the connection. The situation is even further
|
||||
# complicated in case the user requests commandline help.
|
||||
|
||||
# To resolve the problem we first check if the user needs basic help (no '--uri' parameter present). If this is the
|
||||
# case, the program will exit with a basic help screen.
|
||||
check_if_user_needs_basic_help(sys.argv)
|
||||
|
||||
# In all other cases we parse the arguments we know so far. In case the user requests commandline help, we will
|
||||
# ignore this request and continue. The full help is then displayed later when the option_parser is completed
|
||||
# afer we have requested the commandline argument descriptions from the RCP Server. (see below)
|
||||
opts = parse_known_arguemnts(sys.argv)
|
||||
|
||||
# Load SSL/TLS CA certificate from file
|
||||
if opts.ca_cert:
|
||||
ssl_context = load_ca_cert("RCP Server CA", opts.ca_cert)
|
||||
else:
|
||||
ssl_context = None
|
||||
|
||||
# Initialize card reader
|
||||
try:
|
||||
sl = init_reader(opts)
|
||||
sl.connect()
|
||||
except Exception as e:
|
||||
backtrace("Card reader initialization")
|
||||
sys.exit(1)
|
||||
|
||||
# Run the RCP session
|
||||
try:
|
||||
rc = asyncio.run(run_rcp_session(opts, sl, ssl_context))
|
||||
sys.exit(rc)
|
||||
except SystemExit as rc:
|
||||
sys.exit(rc)
|
||||
except:
|
||||
backtrace("RCP session")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Philipp Maier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU 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 os
|
||||
import argparse
|
||||
import logging
|
||||
import threading
|
||||
import asyncio
|
||||
import websockets
|
||||
from argparse import Namespace
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from osmocom.utils import Hexstr, is_hexstr
|
||||
from pySim.utils import ResTuple
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.log import PySimLogger
|
||||
from rcp_utils import SrvSyncConnHdlr, CltConnHdlr, backtrace, pytype_to_type, load_server_cert, load_ca_cert
|
||||
from rcp_utils import dict_from_key_value_pairs, load_json_schema, JsonValidator
|
||||
from rcp_server import RCPM_VERSION_PROTOCOL
|
||||
from websockets.sync.server import serve, ServerConnection
|
||||
from pySim.app import init_card
|
||||
from pySim.runtime import RuntimeState
|
||||
from pySim.cards import CardBase
|
||||
from pySim.card_key_provider import CardKeyFieldCryptor
|
||||
from packaging.version import Version
|
||||
|
||||
# Response timeout towards the RCP Server (includes RCP Client latency)
|
||||
RCP_SERVER_TIMEOUT = 30 # sec.
|
||||
|
||||
log = PySimLogger.get(Path(__file__).stem)
|
||||
|
||||
class RcpsSimLink(LinkBase):
|
||||
"""
|
||||
pySim: Transport Link for RCPM (Remote Card Procedure Module)
|
||||
This is a 'headless' transport link implementation that can only be used from an RCPM module. It merely serves as
|
||||
an adapter between the pySim transport API and the RCPM command server connection handler.
|
||||
"""
|
||||
|
||||
name = 'RCPM'
|
||||
|
||||
def __init__(self, conn_hdlr: SrvSyncConnHdlr, **kwargs):
|
||||
self.conn_hdlr = conn_hdlr
|
||||
self._atr = None
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "rcpm:" + str(self.conn_hdlr)
|
||||
|
||||
def _send_apdu(self, apdu: Hexstr) -> ResTuple:
|
||||
tx_json = {'rcps_instr': {'c_apdu' : apdu.upper()}}
|
||||
rx_json = self.conn_hdlr._transact(tx_json)
|
||||
data = rx_json['rcps_result']['r_apdu']['data']
|
||||
sw = rx_json['rcps_result']['r_apdu']['sw']
|
||||
return data, sw
|
||||
|
||||
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
|
||||
# In this setting, we do not have/cannot to wait for a card since we are not the entity that handles the
|
||||
# direct connection to the card. When the procedure begins, we assume that the remote end already has set up
|
||||
# a connection to the card and made it ready to perform operations on it.
|
||||
pass
|
||||
|
||||
def connect(self):
|
||||
# In this setting, we do not have/cannot to connect because we are not the entity that handles the direct
|
||||
# connection to the card. The connection is established by the remote end.
|
||||
pass
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return self._atr
|
||||
|
||||
def disconnect(self):
|
||||
# In this setting, we do not have/cannot disconnect because we are not the entity that handles the direct
|
||||
# connection to the card. The disconnect is eventually done by the remote end when the procedure has finished.
|
||||
pass
|
||||
|
||||
def _reset_card(self):
|
||||
tx_json = {'rcps_instr': {'reset' : None}}
|
||||
rx_json = self.conn_hdlr._transact(tx_json)
|
||||
self._atr = rx_json['rcps_result']['atr']
|
||||
return 1
|
||||
|
||||
class RcpsCltConnHdlr(CltConnHdlr):
|
||||
"""
|
||||
The RCP Server client handler is used to connect to the RCP Server when RCP Module is started. The connection is
|
||||
kept alive until the RCP Module is terminated. This connection is used to exchange management data with the RCP
|
||||
Server.
|
||||
"""
|
||||
|
||||
def __init__(self, cmd_srv_addr: str, cmd_srv_port: int, module, *args, **kwargs):
|
||||
self.cmd_srv_addr = cmd_srv_addr
|
||||
self.cmd_srv_port = cmd_srv_port
|
||||
self.module = module
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def check_version(self):
|
||||
"""
|
||||
Send the Protocol and Software version of this RCP Module to the RCP Server. The RCP Server and the RCP Module
|
||||
must always use the same protrocol version.
|
||||
"""
|
||||
tx_json = {'rcpm_version': {'protocol' : RCPM_VERSION_PROTOCOL}}
|
||||
rx_json = await self._transact(tx_json)
|
||||
rcpm_version_protocol = Version(rx_json['rcpm_version']['protocol'])
|
||||
if Version(RCPM_VERSION_PROTOCOL) != rcpm_version_protocol:
|
||||
raise ValueError("Incompatible protocol version %s != %s", Version(RCPM_VERSION_PROTOCOL), rcpm_version_protocol)
|
||||
|
||||
async def describe(self):
|
||||
"""
|
||||
Send a detailed description about this RCP Module to the RCP Server. This is also the initial message that
|
||||
the RCP Server expects when an RCP Module connects.
|
||||
"""
|
||||
|
||||
# The rules (dict) in suitable_for (array of dict) may contain hexstrings. Here we go through those rules
|
||||
# and convert those hexstrings to uppercase, since this is the standard we have set for the JSON messages.
|
||||
suitable_for = []
|
||||
for rule in self.module.suitable_for:
|
||||
rule_filtered = {}
|
||||
for k in rule:
|
||||
if is_hexstr(rule[k]):
|
||||
rule_filtered[k] = rule[k].upper()
|
||||
else:
|
||||
rule_filtered[k] = rule[k]
|
||||
suitable_for.append(rule_filtered)
|
||||
|
||||
# Publish RCP Module description on the RCP server
|
||||
tx_json = {'rcpm_hello':
|
||||
{'name' : self.module.name,
|
||||
'cmd_descr' : self.module.cmd_descr,
|
||||
'suitable_for' : suitable_for,
|
||||
'addr' : self.cmd_srv_addr,
|
||||
'port' : self.cmd_srv_port
|
||||
}
|
||||
}
|
||||
rx_json = await self._transact(tx_json)
|
||||
if 'rcpm_welcome' not in rx_json:
|
||||
raise ValueError("description not accepted by RCP Server")
|
||||
|
||||
class RcpModule(abc.ABC):
|
||||
"""
|
||||
Base class to implement to derive a concrete RCP module class
|
||||
"""
|
||||
|
||||
# Module name used to identify the module in logs and user output. This module name should be short and concise.
|
||||
name = "RCPM"
|
||||
|
||||
# Command description of this module. The command description consists of a short and concise command name, a
|
||||
# helpstring and an argument specification in the form of a python dict. This specification, consisting of
|
||||
# 'name', 'help', and 'args' is is directly passed to agparse on the client side.
|
||||
#
|
||||
# In addition to that, the API user may specify which keys the RCP Server shall retrieve before a command is
|
||||
# executed. This is done via the 'get_keys' field. This field is optional and has the form of a dict with
|
||||
# two optional fields 'uicc' and 'euicc'. The value part of both fields is a list of strings which name the
|
||||
# columns that are passed to the CardKeyProvider for lookup. When the 'uicc' field is set, then the RCP Server
|
||||
# will automatically request the ICCID from the card and do the lookup. When the 'euicc' field is set, the RCP
|
||||
# Server will do the same with the EID. It is possible to mix both fields to request keys for the eUICC and the
|
||||
# currently activated eSIM profile at the same time. However, this may be a very rare corner case.
|
||||
#
|
||||
# Example:
|
||||
# cmd_descr = [{'name' : 'reset',
|
||||
# 'help': 'reset the card',
|
||||
# 'args' : []},
|
||||
# {'name' : 'read_binary',
|
||||
# 'help': 'read binary data from a transparent file.',
|
||||
# 'args' : [{ 'name' : '--fid',
|
||||
# 'spec' : {'required' : True,
|
||||
# 'help' : 'File identifier to of the file to read',
|
||||
# 'action' : 'append',
|
||||
# 'pytype' : 'str'},
|
||||
# }
|
||||
# ]},
|
||||
# {'name' : 'unlock_aram',
|
||||
# 'help': 'unlock a locked ARA-M applet on a sysmoISIM-SJA5',
|
||||
# 'args' : [],
|
||||
# 'get_keys' : {'uicc' : ['KIC', 'KID', 'KIK']}}
|
||||
# ]
|
||||
cmd_descr = []
|
||||
|
||||
# Card properties to determine if this module is suitable for a specific card type or card types. The RCP Server
|
||||
# will match those properties against user requests to determine which module provides useful services to the
|
||||
# user's card.
|
||||
#
|
||||
# Example: [{"atr" : "3b9f96803f87828031e073fe211f574543753130136502"}]
|
||||
suitable_for = []
|
||||
|
||||
# In addition the above, the derived class must implement command methods for each command that is defined in the
|
||||
# command description (see above). Each command method must begin with the prefix "cmd_" followed by the command
|
||||
# name used in the command description. A command method must have the form as shown in the example shown below.
|
||||
# Each method should return an integer value which will become the final return code of the RCP client program.
|
||||
#
|
||||
# Args:
|
||||
# hdlr: RcpModuleHdlr object, this object is provided by the RcpmCmdSrvConnHdlr object, which calls
|
||||
# the command method of the module. Through the RcpModuleHdlr object, the API user gets access
|
||||
# to special service methods (e.g. print) and other required properties (e.g. the SimCardCommands
|
||||
# objects, key material and others (see RcpModuleHdlr).
|
||||
#
|
||||
# Example:
|
||||
# def cmd_reset(self, hdlr: RcpModuleHdlr) -> int: ...
|
||||
# def cmd_read_binary(self, hdlr: RcpModuleHdlr) -> int: ...
|
||||
# def cmd_unlock_aram(self, hdlr: RcpModuleHdlr) -> int: ...
|
||||
|
||||
# When the RCP Module class is passed to rcpm_run_module(), rcpm_run_module() also accepts *args and **kwargs
|
||||
# parameter. Those parameters are passed to the constructor of RCP Module class when it is instaniated by
|
||||
# rcpm_run_module(). API may override this constructor (below) with a custom implementation, if required.
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
class RcpmCmdSrvConnHdlr(SrvSyncConnHdlr):
|
||||
"""
|
||||
The RCP Module command server connection handler is used to handle dedicated connections from the RCP Server. Those
|
||||
dedicated connections are technically transparent connections between the RCP Client and the RCP Module (this). The
|
||||
RCP Server merely acts as a proxy at that point.
|
||||
"""
|
||||
|
||||
def __init__(self, module: RcpModule, field_cryptor: CardKeyFieldCryptor, *args, **kwargs):
|
||||
SrvSyncConnHdlr.__init__(self, *args, *kwargs)
|
||||
self.module = module
|
||||
self.crypt = field_cryptor
|
||||
|
||||
def _parse_cmd_argv(self, cmd_suffix: str, cmd_argv: list[str]) -> Namespace:
|
||||
""" Parse (and validate) the received argument vector """
|
||||
# Use the cmd_descr of the module to create a (temporary) argument parser for the received argument vector.
|
||||
cmd_parser = argparse.ArgumentParser()
|
||||
for cmd in self.module.cmd_descr:
|
||||
if cmd['name'] == cmd_suffix:
|
||||
args = deepcopy(cmd['args'])
|
||||
for arg in args:
|
||||
arg['spec'] = pytype_to_type(arg['spec'])
|
||||
cmd_parser.add_argument(arg['name'], **arg['spec'])
|
||||
|
||||
# Parse the arguments and return the parsed Namespace object.
|
||||
try:
|
||||
return cmd_parser.parse_args(cmd_argv)
|
||||
except SystemExit:
|
||||
raise ValueError("unable to parse arguments: %s", str(cmd_argv), )
|
||||
|
||||
def print(self, message: str):
|
||||
""" Print a message on the client side """
|
||||
log.info(str(self) + " -- %s" % message)
|
||||
tx_json = {'rcps_instr': {'print' : message}}
|
||||
rx_json = self._transact(tx_json)
|
||||
if rx_json != {'rcps_result': {'empty' : None}}:
|
||||
raise ValueError("unexpected response from RCP Client: %s", rx_json)
|
||||
|
||||
def procedure(self):
|
||||
""" Receive and process a command from the RCP Client (via RCP Server) """
|
||||
|
||||
# Receive the command request.
|
||||
rx_json = self._recv()
|
||||
cmd = rx_json['rcps_command']['cmd']
|
||||
cmd_argv = rx_json['rcps_command']['cmd_argv']
|
||||
keys = rx_json['rcps_command'].get('keys')
|
||||
log.info(str(self) + " -- executing command: %s %s", cmd, str(cmd_argv))
|
||||
|
||||
try:
|
||||
# Make sure the command actually addresses this module.
|
||||
cmd_prefix = self.module.name + "_"
|
||||
if not cmd.startswith(cmd_prefix):
|
||||
raise ValueError("invalid command: %s" % cmd)
|
||||
|
||||
# Make sure the module actually provides a command method for the requested command.
|
||||
cmd_suffix = cmd[len(cmd_prefix):]
|
||||
cmd_method = "cmd_" + cmd_suffix
|
||||
if not hasattr(self.module, cmd_method):
|
||||
raise ValueError("missing command method: %s" % cmd_method)
|
||||
|
||||
# Parse and validate command arguments.
|
||||
cmd_args = self._parse_cmd_argv(cmd_suffix, cmd_argv)
|
||||
|
||||
# Setup a pySim RuntimeState, CardBase and a RuntimeLchan.
|
||||
rs, card = init_card(RcpsSimLink(self))
|
||||
|
||||
# Hand over control to the command method provided by the specific module implementation.
|
||||
rcp_module_hdlr = RcpModuleHdlr(self.print, rs, card, cmd_args, keys, self.crypt)
|
||||
rs.reset()
|
||||
try:
|
||||
rc = getattr(self.module, cmd_method)(rcp_module_hdlr)
|
||||
except Exception as e:
|
||||
backtrace("command method")
|
||||
rc = 1 # general error
|
||||
|
||||
except Exception as e:
|
||||
backtrace("command parsing")
|
||||
rc = 126 # cannot execute
|
||||
|
||||
# The prodedure is done, send "goodbye" message.
|
||||
log.info(str(self) + " -- command execution done, rc: %d" % rc)
|
||||
tx_json = {'rcps_goodbye': rc}
|
||||
self._send(tx_json)
|
||||
|
||||
class RcpModuleHdlr():
|
||||
"""
|
||||
RCP Module handler class. This class is used by the RcpmCmdSrvConnHdlr to create the handler RcpModuleHdlr object
|
||||
(hdlr), which is is passed to the command method. The RcpModuleHdlr gives the API user access to resources he can
|
||||
use carry out the command.
|
||||
"""
|
||||
|
||||
# The RuntimeState (rs), the CardBase (card) and the RuntimeLchan (lchan) are the three major objects through which
|
||||
# an API user may interact with the UICC/eUICC on the other remote end. Those objects have the same objectives as
|
||||
# in pySim-shell.py, with lchan representing the currently selected lchan (set to self.rs.lchan[0] by default, API
|
||||
# users may change the reference to a different lchan)
|
||||
rs = None
|
||||
card = None
|
||||
lchan = None
|
||||
|
||||
# The cmd_args property contains the parsed command arguments which were passed by the end-user to the RCP Client.
|
||||
# The arguments are already parsed and validated against the cmd_dscr property of the RcpModule. The arguments are
|
||||
# in the form of a Namespace object and can be accessed like any argparse output. However, since the arguments
|
||||
# contain user input, some caution is required.
|
||||
cmd_args = None
|
||||
|
||||
# In case the retrieve_uicc_keys property of the RcpModule is used retrieve UICC key material, this property will
|
||||
# contain the key material in the form of a dictionary. The format is similar to the return value of
|
||||
# card_key_provider_get() (see also pySim.card_key_provider).
|
||||
keys_uicc = {}
|
||||
|
||||
# Same as self.keys_uicc, but contains eUICC related key material in case requested using retrieve_uicc_keys.
|
||||
keys_euicc = {}
|
||||
|
||||
def __init__(self, print: callable, rs: RuntimeState, card: CardBase, cmd_args: Namespace,
|
||||
keys: dict, field_cryptor: CardKeyFieldCryptor):
|
||||
self.print = print
|
||||
self.rs = rs
|
||||
self.card = card
|
||||
self.lchan = self.rs.lchan[0]
|
||||
self.cmd_args = cmd_args
|
||||
if keys:
|
||||
if 'uicc' in keys:
|
||||
self.keys_uicc = dict_from_key_value_pairs(keys['uicc'], keylabel='key', valuelabel='value')
|
||||
for key in self.keys_uicc.keys():
|
||||
self.keys_uicc[key] = field_cryptor.decrypt_field(key, self.keys_uicc.get(key))
|
||||
if 'euicc' in keys:
|
||||
self.keys_euicc = dict_from_key_value_pairs(keys['euicc'], keylabel='key', valuelabel='value')
|
||||
for key in self.keys_euicc.keys():
|
||||
self.keys_euicc[key] = field_cryptor.decrypt_field(key, self.keys_euicc.get(key))
|
||||
|
||||
def rcpm_setup_argparse(description: str):
|
||||
"""Create argument parser and add the basic arguments all RCP Modules should have"""
|
||||
|
||||
option_parser = argparse.ArgumentParser(description='RCP Module: ' + description,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
|
||||
option_parser.add_argument("--uri", help="URI of the RCP-Server", required=True)
|
||||
option_parser.add_argument("--rcps-ca-cert", help="SSL/TLS CA-Certificate of the RCP-Server", required=True)
|
||||
option_parser.add_argument("--rcpm-cmd-server-addr", help="Local Host/IP to bind RCP-Module-Command-Server to",
|
||||
required=True)
|
||||
option_parser.add_argument("--rcpm-cmd-server-port", help="Local TCP port to bind RCP-Module-Command-Server to",
|
||||
required=True, type=int)
|
||||
option_parser.add_argument("--rcpm-cmd-server-cert", help="SSL/TLS Certificate of the RCP-Module-Command-Server",
|
||||
required=True)
|
||||
CardKeyFieldCryptor.argparse_add_args(option_parser)
|
||||
return option_parser
|
||||
|
||||
def rcpm_run_module(opts: Namespace, module: RcpModule, *args, **kwargs):
|
||||
|
||||
PySimLogger.setup(print, {logging.WARN: "\033[33m", logging.DEBUG: "\033[90m"}, opts.verbose)
|
||||
log.info("RCP Module startup: %s", module.name)
|
||||
log.debug("Main process ID: %d", os.getpid())
|
||||
|
||||
# Load SSL/TLS certificates.
|
||||
rcpm_cmd_ssl_context = load_server_cert("RCPM Command Server", opts.rcpm_cmd_server_cert)
|
||||
ssl_context = load_ca_cert("RCPM Server Client", opts.rcps_ca_cert)
|
||||
|
||||
# Load JSON schema for message validation between RCP Server and RCP Module (this process)
|
||||
rcpm_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpm_to_rcps_schema.json"))
|
||||
rcps_to_rcpm_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpm_schema.json"))
|
||||
|
||||
# Load JSON schema for message validation between RCP Server and RCP Module Command Server (this process)
|
||||
rcpmcs_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpmcs_to_rcps_schema.json"))
|
||||
rcps_to_rcpmcs_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpmcs_schema.json"))
|
||||
|
||||
# Start local RCP Client Command Server.
|
||||
log.info("RCPC command server at: %s:%d" % (opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port))
|
||||
def rcpm_cmd_conn_hdlr(websocket: ServerConnection):
|
||||
json_validator = JsonValidator(rcps_to_rcpmcs_schema, rcpmcs_to_rcps_schema)
|
||||
transport_keys = CardKeyFieldCryptor.transport_keys_from_opts(opts)
|
||||
field_cryptor = CardKeyFieldCryptor(transport_keys)
|
||||
hdlr = RcpmCmdSrvConnHdlr(module(*args, *kwargs), field_cryptor, websocket, RCP_SERVER_TIMEOUT, json_validator)
|
||||
hdlr.procedure()
|
||||
hdlr.close()
|
||||
|
||||
server = serve(rcpm_cmd_conn_hdlr, opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port, ssl=rcpm_cmd_ssl_context)
|
||||
def rcpm_cmd_server():
|
||||
log.debug("RCPC command server thread ID: %d", threading.get_native_id())
|
||||
server.serve_forever()
|
||||
rcpm_cmd_server_thread = threading.Thread(target = rcpm_cmd_server)
|
||||
rcpm_cmd_server_thread.start()
|
||||
|
||||
# Connect to RCP Server and publish module description.
|
||||
async def rcps_client():
|
||||
async with websockets.connect(opts.uri, ping_timeout=10.0, ping_interval=1.0, ssl=ssl_context) as websocket:
|
||||
json_validator = JsonValidator(rcps_to_rcpm_schema, rcpm_to_rcps_schema)
|
||||
client = RcpsCltConnHdlr(opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port, module, websocket,
|
||||
RCP_SERVER_TIMEOUT, json_validator)
|
||||
await client.check_version()
|
||||
await client.describe()
|
||||
await client.wait_close()
|
||||
try:
|
||||
asyncio.run(rcps_client())
|
||||
except Exception as e:
|
||||
backtrace("RCPS client")
|
||||
|
||||
# Shutdown
|
||||
server.shutdown()
|
||||
rcpm_cmd_server_thread.join()
|
||||
log.info("RCP Module shutdown: %s", module.name)
|
||||
@@ -1,666 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Philipp Maier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU 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 os
|
||||
import sys
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import websockets
|
||||
from osmocom.utils import Hexstr
|
||||
from pySim.utils import ResTuple
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from pySim.log import PySimLogger
|
||||
from pySim.utils import dec_iccid
|
||||
from websockets.asyncio.server import serve, ServerConnection
|
||||
from rcp_utils import SrvConnHdlr, CltConnHdlr, JsonValidator, FlightRecorder
|
||||
from rcp_utils import load_json_schema, backtrace, pytype_to_type, load_server_cert, load_ca_cert
|
||||
from rcp_utils import key_value_pairs_from_dict
|
||||
from pySim.card_key_provider import card_key_provider_argparse_add_args, card_key_provider_init
|
||||
from pySim.card_key_provider import card_key_provider_get_field, card_key_provider_get
|
||||
from packaging.version import Version
|
||||
|
||||
CLIENT_TIMEOUT = 10
|
||||
|
||||
# The protocol version between the RCP Server and the RCP Module must always match up. In case there as changes to
|
||||
# the protocol (JSON Schema and/or application logic). This version number shall be incremented accordingly. Since
|
||||
# RCP Modules usually run from the same pySim modules as the RCP Server, a change to this version number should
|
||||
# not affect the RCP Module implementation itself.
|
||||
RCPM_VERSION_PROTOCOL = "1.0.0"
|
||||
|
||||
# The RCP Server software version shall be incremented when there are changes to the RCP Sever (this module) or changes
|
||||
# to other related modules, which affect the RCP Server. The RCP Server software version is also disclosed towards the
|
||||
# RCP Client.
|
||||
RCPS_VERSION_SOFTWARE = "1.0.0"
|
||||
|
||||
# The RCP Server protocol version refers to the protocol spoken between RCP Client and RCP Server. The protocol version
|
||||
# shall be incremented when there are changes to the protocol (JSON Schema and/or application logic). When an
|
||||
# RCP Client connects, this protocol version is compared against the protocol version that the client sends
|
||||
# (see also RCPC_VERSION_PROTOCOL in rcp_client.py). It is up to the RCP Server to decide whether or not a deviation
|
||||
# between protocol versions is tolerable or not.
|
||||
RCPS_VERSION_PROTOCOL = "1.0.0"
|
||||
|
||||
log = PySimLogger.get(Path(__file__).stem)
|
||||
runtime_state = None
|
||||
rate_limiter = None
|
||||
option_parser = argparse.ArgumentParser(description='RCP Server',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
option_parser.add_argument("--verbose", help="Enable verbose logging",
|
||||
action='store_true', default=False)
|
||||
option_parser.add_argument("--rcpc-server-addr", help="Local Host/IP to bind RCP-Client-Server to",
|
||||
required=True)
|
||||
option_parser.add_argument("--rcpc-server-port", help="Local TCP port to bind RCP-Client-Server to",
|
||||
required=True, type=int)
|
||||
option_parser.add_argument("--rcpc-server-cert", help="SSL/TLS Certificate of the RCP-Client-Server",
|
||||
required=True)
|
||||
option_parser.add_argument("--rcpc-request-limit", help="number of RCP Client requests per minute",
|
||||
default=600)
|
||||
option_parser.add_argument("--rcpm-server-addr", help="Local Host/IP to bind RCP-Module-Server to",
|
||||
required=True)
|
||||
option_parser.add_argument("--rcpm-server-port", help="Local TCP port to bind RCP-Module-Server to",
|
||||
required=True, type=int)
|
||||
option_parser.add_argument("--rcpm-server-cert", help="SSL/TLS Certificate of the RCP-Module-Server",
|
||||
required=True)
|
||||
option_parser.add_argument("--rcpm-module-ca-cert", help="SSL/TLS CA-Certificate of the RCP-Module-Command-Server",
|
||||
required=True)
|
||||
option_parser.add_argument("--open-observe-url", help="OpenObserve API endpoint URL")
|
||||
option_parser.add_argument("--open-observe-email", help="OpenObserve service email address")
|
||||
option_parser.add_argument("--open-observe-token", help="OpenObserve service token")
|
||||
|
||||
card_key_provider_argparse_add_args(option_parser)
|
||||
|
||||
class ModuleRuntimeState:
|
||||
def __init__(self, websocket:ServerConnection, name:str, cmd_descr:list, suitable_for:list, addr:str, port:int):
|
||||
self.name = name
|
||||
self.websocket = websocket
|
||||
|
||||
# Run the cmd_descr through argparse to catch malformed argument specifications early
|
||||
for cmd in cmd_descr:
|
||||
args = deepcopy(cmd['args'])
|
||||
cmd_parser = argparse.ArgumentParser()
|
||||
for arg in args:
|
||||
try:
|
||||
arg['spec'] = pytype_to_type(arg['spec'])
|
||||
cmd_parser.add_argument(arg['name'], **arg['spec'])
|
||||
except:
|
||||
raise ValueError("invalid argument spec %s -- check RCP Module" % str(arg))
|
||||
|
||||
self.cmd_descr = cmd_descr
|
||||
self.suitable_for = suitable_for
|
||||
self.addr = addr
|
||||
self.port = port
|
||||
log.debug("new RCP Module context created: '%s'", name)
|
||||
|
||||
def is_suitable(self, suitable_for:dict) -> bool:
|
||||
"""Check if this module is 'suitable_for' a specific card"""
|
||||
if suitable_for in self.suitable_for:
|
||||
return True
|
||||
return False
|
||||
|
||||
def describe(self) -> dict:
|
||||
"""Describe this module towards the RCP Client"""
|
||||
|
||||
# The command description sent by the RCP Module also includes fields that are intended to be seen
|
||||
# only by the RCP Server. Here we set up the command description as it is expected by the RCP Client.
|
||||
cmd_descr = []
|
||||
for descr in self.cmd_descr:
|
||||
cmd_descr.append({'name' : descr['name'],
|
||||
'help' : descr['help'],
|
||||
'args' : descr['args']})
|
||||
|
||||
# Return module description
|
||||
return {'name': self.name,
|
||||
'cmd_descr': cmd_descr}
|
||||
|
||||
def get_cmd_descr(self, cmd: str) -> dict:
|
||||
"""Get the description for a specific command of this module"""
|
||||
for descr in self.cmd_descr:
|
||||
if self.name + "_" + descr['name'] == cmd:
|
||||
return descr
|
||||
raise ValueError("command %s not found in command description %s" % (cmd_name, str(self.cmd_descr)))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __del__(self):
|
||||
log.debug("RCP module context destroyed: '%s'", self.name)
|
||||
|
||||
class RuntimeState:
|
||||
def __init__(self, rcpm_ca_ssl_context, open_observe_pars):
|
||||
self.module_runtime_states = []
|
||||
self.rcpm_ca_ssl_context = rcpm_ca_ssl_context
|
||||
self.open_observe_pars = open_observe_pars
|
||||
|
||||
# Load JSON schema for message validation between RCP Client and RCP Server (this process)
|
||||
self.rcpc_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
|
||||
"rcpc_to_rcps_schema.json"))
|
||||
self.rcps_to_rcpc_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
|
||||
"rcps_to_rcpc_schema.json"))
|
||||
|
||||
# Load JSON schema for message validation between RCP Module and RCP Server (this process)
|
||||
self.rcpm_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
|
||||
"rcpm_to_rcps_schema.json"))
|
||||
self.rcps_to_rcpm_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
|
||||
"rcps_to_rcpm_schema.json"))
|
||||
|
||||
# Load JSON schema for message validation between RCP Module Command Server and RCP Server (this process)
|
||||
self.rcpmcs_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
|
||||
"rcpmcs_to_rcps_schema.json"))
|
||||
self.rcps_to_rcpmcs_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
|
||||
"rcps_to_rcpmcs_schema.json"))
|
||||
|
||||
log.debug("new runtime context created.")
|
||||
|
||||
def __log_modules_available(self) -> str:
|
||||
if self.module_runtime_states:
|
||||
modules_str = ""
|
||||
for module in self.module_runtime_states:
|
||||
modules_str += "'" + str(module) + "', "
|
||||
return "RCP modules available: %s" % modules_str[:-2]
|
||||
else:
|
||||
return "RCP modules available: none"
|
||||
|
||||
def module_add(self, module: ModuleRuntimeState):
|
||||
self.module_runtime_states.append(module)
|
||||
log.info("new RCP module, %s", self.__log_modules_available())
|
||||
|
||||
def module_remove(self, websocket:ServerConnection):
|
||||
for module in self.module_runtime_states:
|
||||
if module.websocket == websocket:
|
||||
self.module_runtime_states.remove(module)
|
||||
log.info("RCP module removed, %s", self.__log_modules_available())
|
||||
return
|
||||
log.warning("cannot remove RCP module, no RCP module associated with RCPC connection: %s:%d, %s" %
|
||||
(*websocket.remote_address, self.__log_modules_available()))
|
||||
|
||||
def modules_find(self, suitable_for:dict) -> list[dict]:
|
||||
modules = []
|
||||
for module in self.module_runtime_states:
|
||||
if module.is_suitable(suitable_for):
|
||||
modules.append(module.describe())
|
||||
if modules:
|
||||
return modules
|
||||
# It is absolutely tolerable if no suitable RCP module can be found. If this is the case, the client should
|
||||
# display an empty help screen and exit normally.
|
||||
log.warning("no suitable RCP module found, %s", self.__log_modules_available())
|
||||
return []
|
||||
|
||||
def module_find(self, suitable_for:dict, cmd:str) -> ModuleRuntimeState:
|
||||
modules = self.modules_find(suitable_for)
|
||||
for m in modules:
|
||||
module_name = m['name']
|
||||
cmd_descr = m['cmd_descr']
|
||||
for c in cmd_descr:
|
||||
cmd_name = c['name']
|
||||
if module_name + "_" + cmd_name == cmd:
|
||||
break
|
||||
for module_runtime_state in self.module_runtime_states:
|
||||
if module_runtime_state.name == module_name:
|
||||
return module_runtime_state
|
||||
# Normally we should find the RCP module. When this method is called, we have already called modules_find
|
||||
# before because we had to return the command descriptions to the client. If we cannot find the RCP module
|
||||
# now, the module have been disconnected or the client somehow called a command that does not exist. In any
|
||||
# case, ending up here means we cannot continue.
|
||||
raise ValueError("RCP module not found for command: %s, " % (cmd, self.__log_modules_available()))
|
||||
|
||||
class RcpmCltConnHdlr(CltConnHdlr):
|
||||
"""
|
||||
The RCP Module client connection handler is the dedicated client that is used by the RCP Client connection handler
|
||||
to handle the dedicated connection towards the RCP Module (see below)
|
||||
"""
|
||||
|
||||
class RcpcSrvConnHdlr(SrvConnHdlr):
|
||||
"""
|
||||
The RCP Client connection handler takes care of the handling of client requests. Throughout the lifetime of a
|
||||
connection, the client will request a description of the available commands and then request the execution of a
|
||||
procedure. To execute the procedure, the handler will make a dedicated connection to the RCP Module and then
|
||||
transparently pass the messages from the RCP Client to the RCP Module and vice versa.
|
||||
"""
|
||||
|
||||
module_client = None
|
||||
|
||||
async def check_version(self):
|
||||
"""
|
||||
Check the RCP Client software and protocol version to ensure the requesting RCP Client is compatible with this
|
||||
RCP Server version.
|
||||
"""
|
||||
|
||||
# Receive version info from RCP client
|
||||
rx_json = await self._recv()
|
||||
rcpc_version_software = Version(rx_json['rcpc_version']['software'])
|
||||
rcpc_version_protocol = Version(rx_json['rcpc_version']['protocol'])
|
||||
log.debug("RCP Client version: software=%s, protocol=%s",
|
||||
rcpc_version_software, rcpc_version_protocol)
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_meta('rcpc_version_software', str(rcpc_version_software))
|
||||
self.flight_recorder.record_meta('rcpc_version_protocol', str(rcpc_version_protocol))
|
||||
|
||||
# Check if the RCP Client is compatible with this RCP Server. As of now we expect that the client uses the
|
||||
# exact same protocol version as the server.
|
||||
rcpc_version_protocol_expected = Version(RCPS_VERSION_PROTOCOL)
|
||||
if rcpc_version_protocol != rcpc_version_protocol_expected:
|
||||
info = "RCP Client uses unsupported protocol version (%s != %s)" % (rcpc_version_protocol, rcpc_version_protocol_expected)
|
||||
raise_exception = True
|
||||
else:
|
||||
info = None
|
||||
raise_exception = False
|
||||
|
||||
# Respond with RCP Server version info. We do this before we potentially raise an exception to make sure the
|
||||
# RCP Server version info arrives at the client.
|
||||
tx_json = {'rcpc_version': {'software' : RCPS_VERSION_SOFTWARE,
|
||||
'protocol' : RCPS_VERSION_PROTOCOL}}
|
||||
if info:
|
||||
tx_json['rcpc_version']['info'] = info
|
||||
await self._send(tx_json)
|
||||
|
||||
# Raise exception in case problems were detected. This will close the connection, but the client still has the
|
||||
# version info (see above)
|
||||
if raise_exception:
|
||||
raise ValueError(info)
|
||||
|
||||
async def describe(self):
|
||||
"""
|
||||
Collect the command/argument description of suitable modules and forward that definition to the RCP client. The
|
||||
RCP client will then build an argument parser (commandline help, argument validation) from this information.
|
||||
"""
|
||||
rx_json = await self._recv()
|
||||
self.suitable_for = rx_json['rcpc_hello']['suitable_for']
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_meta('suitable_for', self.suitable_for)
|
||||
modules = runtime_state.modules_find(self.suitable_for)
|
||||
if self.flight_recorder:
|
||||
suitable_modules = []
|
||||
for m in modules:
|
||||
suitable_modules.append(m['name'])
|
||||
self.flight_recorder.record_meta('suitable_modules', suitable_modules)
|
||||
tx_json = {'rcpc_welcome':
|
||||
{'module_descr' : modules}
|
||||
}
|
||||
await self._send(tx_json)
|
||||
|
||||
async def _transact_apdu(self, apdu: Hexstr) -> ResTuple:
|
||||
"""Private low level method to exchange an APDU"""
|
||||
tx_json = {'rcpc_instr': {'c_apdu' : apdu.upper()}}
|
||||
rx_json = await self._transact(tx_json)
|
||||
if rx_json is None:
|
||||
raise ValueError("RCP Client vanished unexpectetly")
|
||||
data = rx_json['rcpc_result']['r_apdu']['data']
|
||||
sw = rx_json['rcpc_result']['r_apdu']['sw']
|
||||
return data, sw
|
||||
|
||||
async def _reset(self) -> Hexstr:
|
||||
"""Private low level method to reset the UICC/eUICC"""
|
||||
tx_json = {'rcpc_instr': {'reset' : None}}
|
||||
rx_json = await self._transact(tx_json)
|
||||
if rx_json is None:
|
||||
raise ValueError("RCP Client vanished unexpectetly")
|
||||
return rx_json['rcpc_result']['atr']
|
||||
|
||||
async def _read_iccid(self) -> Hexstr:
|
||||
"""Private low level method to read the EID from an UICC (or eSIM)"""
|
||||
data, sw = await self._transact_apdu("00A40000022FE200")
|
||||
if sw != "9000":
|
||||
raise ValueError("Unable to select EF.ICCID, sw: %s, " % sw)
|
||||
data, sw = await self._transact_apdu("00B000000A")
|
||||
if sw != "9000":
|
||||
raise ValueError("Unable to read EF.ICCID, sw: %s, " % sw)
|
||||
return dec_iccid(data)
|
||||
|
||||
async def _read_eid(self) -> Hexstr:
|
||||
"""Private low level method to read the EID from an eUICC"""
|
||||
data, sw = await self._transact_apdu("00A4040410A0000005591010FFFFFFFF890000010000")
|
||||
if sw != "9000":
|
||||
raise ValueError("Unable to select ISD-R, sw: %s, " % sw)
|
||||
data, sw = await self._transact_apdu("80E2910006BF3E035C015A00")
|
||||
if sw != "9000":
|
||||
raise ValueError("Unable to retrieve EID, sw: %s, " % sw)
|
||||
return data[10:]
|
||||
|
||||
async def print(self, message: str):
|
||||
""" Print a message on the client side """
|
||||
tx_json = {'rcpc_instr': {'print' : message}}
|
||||
rx_json = await self._transact(tx_json)
|
||||
if rx_json is None:
|
||||
raise ValueError("RCP Client vanished unexpectedly")
|
||||
if rx_json != {'rcpc_result': {'empty' : None}}:
|
||||
raise ValueError("unexpected response from RCP Client: %s" % rx_json)
|
||||
|
||||
async def procedure(self):
|
||||
"""
|
||||
Receive a command from the client, pick a matching module, make a dedicated connection to that module and
|
||||
forward instruction/response messages between RCP Client and RCP Module until the procedure is done.
|
||||
"""
|
||||
# Receive a command from the client.
|
||||
rx_json = await self._recv()
|
||||
|
||||
# The procedure step is not mandatory. In case no procedure shall be executed, the client may close the
|
||||
# connection early on his behalf. This is normal behavior and usually the case when the user instructs the
|
||||
# RCP client to display the commandline help screens.
|
||||
if rx_json is None:
|
||||
log.debug(str(self) + " -- RCP client has closed the connection, no procedure executed")
|
||||
return
|
||||
|
||||
# The RCP client has sent a command, so we continue with the procedure.
|
||||
command = rx_json['rcpc_command']
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_meta('cmd', command['cmd'])
|
||||
self.flight_recorder.record_meta('cmd_argv', command['cmd_argv'])
|
||||
|
||||
# Pick the matching RCP Module
|
||||
module = runtime_state.module_find(self.suitable_for, command['cmd'])
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_meta('module', module.name)
|
||||
|
||||
# Retrieve keys (if the command requires them)
|
||||
cmd_descr = module.get_cmd_descr(command['cmd'])
|
||||
get_keys = cmd_descr.get('get_keys')
|
||||
if get_keys:
|
||||
keys = {}
|
||||
get_keys_uicc = get_keys.get('uicc')
|
||||
if get_keys_uicc:
|
||||
iccid = await self._read_iccid()
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_meta('iccid', iccid)
|
||||
keys_uicc = card_key_provider_get(get_keys_uicc, 'ICCID', iccid)
|
||||
keys['uicc'] = key_value_pairs_from_dict(keys_uicc, keylabel='key', valuelabel='value')
|
||||
get_keys_euicc = get_keys.get('euicc')
|
||||
if get_keys_euicc:
|
||||
eid = await self._read_eid()
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_meta('eid', eid)
|
||||
keys_euicc = card_key_provider_get(get_keys_euicc, 'EID', eid)
|
||||
keys['euicc'] = key_value_pairs_from_dict(keys_euicc, keylabel='key', valuelabel='value')
|
||||
command['keys'] = keys
|
||||
|
||||
# Resetting card to ensure the card is in a defined state
|
||||
await self._reset()
|
||||
|
||||
# Create a dedicated connection to the RCP Module and proxy the messages between RCP Client and RCP Module.
|
||||
module_uri = "wss://%s:%d" % (module.addr, module.port)
|
||||
log.info(str(self) + " -- executing procedure for command \"%s\" on module \"%s\" at: %s" %
|
||||
(command['cmd'], module.name, module_uri))
|
||||
async with websockets.connect(module_uri, ssl=runtime_state.rcpm_ca_ssl_context) as websocket:
|
||||
# Create a connection to the RCP Module Command Server
|
||||
json_validator = JsonValidator(runtime_state.rcpmcs_to_rcps_schema, runtime_state.rcps_to_rcpmcs_schema)
|
||||
self.module_client = RcpmCltConnHdlr(websocket, CLIENT_TIMEOUT, json_validator, self.flight_recorder)
|
||||
|
||||
# Prepare initial request to be send to the RCP Module Command Server
|
||||
module_tx_json = {'rcps_command' : command}
|
||||
|
||||
# Forward messages between RCP Module Command Server and RCP Client until the procedure ends.
|
||||
while(True):
|
||||
# Send request to the RCP Module Command Server
|
||||
module_rx_json = await self.module_client._transact(module_tx_json)
|
||||
|
||||
# Forward the response to the RCP Client
|
||||
if 'rcps_instr' in module_rx_json:
|
||||
client_tx_json = {'rcpc_instr' : module_rx_json['rcps_instr']}
|
||||
await self._send(client_tx_json)
|
||||
elif 'rcps_goodbye' in module_rx_json:
|
||||
rc = module_rx_json['rcps_goodbye']
|
||||
log.info(str(self) + " -- command execution done, rc: %d" % rc)
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_meta('rc', rc)
|
||||
if rc != 0:
|
||||
self.flight_recorder.crash_report()
|
||||
client_tx_json = {'rcpc_goodbye' : rc}
|
||||
await self._send(client_tx_json)
|
||||
break
|
||||
else:
|
||||
raise ValueError("Unexpected response from RCP Module: %s" % str(module_rx_json))
|
||||
|
||||
# Receive the Result from the client, prepare request (module_tx_json) for the next turn
|
||||
client_rx_json = await self._recv()
|
||||
if client_rx_json is None:
|
||||
raise ValueError("RCP client vanished unexpectedly")
|
||||
if 'rcpc_result' in client_rx_json:
|
||||
module_tx_json = {'rcps_result' : client_rx_json['rcpc_result']}
|
||||
else:
|
||||
raise ValueError("Unexpected result from RCP Client: %s" % str(client_rx_json))
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Close the connection towards the RCP Module Command Server, then close the connection towards the RCP Client.
|
||||
"""
|
||||
if self.module_client:
|
||||
await self.module_client.close()
|
||||
await super().close()
|
||||
|
||||
class RcpmSrvConnHdlr(SrvConnHdlr):
|
||||
"""
|
||||
The RCP Module connection handler is responsible to handle connect and disconnect events of RCP Modules. This
|
||||
connection between the RCP Module and the RCP Server is used for management purposes only.
|
||||
"""
|
||||
|
||||
async def check_version(self):
|
||||
"""
|
||||
Send the Protocol and Software version of this RCP Module to the RCP Server. The RCP Server and the RCP Module
|
||||
must always use the same protocol version.
|
||||
"""
|
||||
tx_json = {'rcpm_version': {'protocol' : RCPM_VERSION_PROTOCOL}}
|
||||
rx_json = await self._transact(tx_json)
|
||||
rcpm_version_protocol = Version(rx_json['rcpm_version']['protocol'])
|
||||
if Version(RCPM_VERSION_PROTOCOL) != rcpm_version_protocol:
|
||||
raise ValueError("Incompatible protocol version %s != %s", Version(RCPM_VERSION_PROTOCOL), rcpm_version_protocol)
|
||||
|
||||
async def describe(self):
|
||||
"""
|
||||
Receive the module description from an RCP Module. This description will be stored in an internal list until
|
||||
the module is disconnected from the server.
|
||||
"""
|
||||
rx_json = await self._recv()
|
||||
runtime_state.module_add(module = ModuleRuntimeState(self.websocket, **rx_json['rcpm_hello']))
|
||||
tx_json = {'rcpm_welcome': None}
|
||||
await self._send(tx_json)
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Remove RCPM from internal list when the connection is closed (and the handler is deleted)
|
||||
"""
|
||||
runtime_state.module_remove(self.websocket)
|
||||
super().__del__()
|
||||
|
||||
class RateLimiter():
|
||||
"""
|
||||
Rate limiter: A rate limiter can be used to limit the amount of requests
|
||||
per interval. Once the interval expires, the request counter is reset and
|
||||
the requestor gets a new request budget to spend.
|
||||
"""
|
||||
|
||||
def __init__(self, interval:int, requests:int):
|
||||
"""
|
||||
Args:
|
||||
interval: reset interval after which request counter is reset.
|
||||
requests: maximum number of requests per interval.
|
||||
Returns:
|
||||
True when rate limit has been exceeded, False otherwise.
|
||||
"""
|
||||
self.table = {}
|
||||
self.interval = interval
|
||||
self.requests = requests
|
||||
self.last_collect = time.time()
|
||||
log.info("Rate-Limit: max %d requests per sec.", self.requests / self.interval)
|
||||
|
||||
def __collect_expired(self):
|
||||
new_table = {}
|
||||
for key in self.table.keys():
|
||||
if time.time() - self.table[key]['timestamp'] <= self.interval:
|
||||
new_table[key] = self.table[key]
|
||||
self.table = new_table
|
||||
|
||||
def limit(self, address:str) -> bool:
|
||||
"""
|
||||
Rate limit request
|
||||
|
||||
Args:
|
||||
address: requestor address
|
||||
Returns:
|
||||
True when rate limit has been exceeded, False otherwise
|
||||
"""
|
||||
|
||||
timestamp = time.time()
|
||||
|
||||
# Collect expired entries once per minute
|
||||
if time.time() - self.last_collect > 60:
|
||||
self.__collect_expired()
|
||||
self.last_collect = timestamp
|
||||
|
||||
# In case no entry exists yet, create a new one => don't block
|
||||
if address not in self.table:
|
||||
self.table[address] = {'timestamp' : timestamp, 'counter' : 1}
|
||||
log.debug("Rate-Limit: %s (new, counter=%d, next reset in %d sec.)",
|
||||
address, 1, self.interval)
|
||||
return False
|
||||
|
||||
# We have to access multiple times, so its better to story the entry
|
||||
# in a temporary variable.
|
||||
entry = self.table[address]
|
||||
|
||||
# If the entry has expired - delete it => don't block
|
||||
if timestamp - entry['timestamp'] > self.interval:
|
||||
log.debug("Rate-Limit: %s (reset, counter=%d, next reset in %d sec.)",
|
||||
address, 1, self.interval)
|
||||
self.table[address] = {'timestamp' : timestamp, 'counter' : 1}
|
||||
return False
|
||||
|
||||
# If the rate limit has been reached => block
|
||||
if entry['counter'] >= self.requests:
|
||||
log.warning("Rate-Limit: %s (exceeded, counter=%d, next reset in %d sec.)",
|
||||
address, entry['counter'], self.interval - (timestamp - entry['timestamp']))
|
||||
return True
|
||||
|
||||
# Increment counter, don't block
|
||||
entry['counter'] += 1
|
||||
log.debug("Rate-Limit: %s (incrementing, counter=%d, next reset in %d sec.)",
|
||||
address, entry['counter'], self.interval - (timestamp - entry['timestamp']))
|
||||
self.table[address] = entry
|
||||
return False
|
||||
|
||||
class OpenObserveFlightRecorder(FlightRecorder):
|
||||
"""Concrete implementation of a "flight recorder" using OpenObserve as a monitoring entity."""
|
||||
|
||||
def __init__(self, url: str, email: str, token: str):
|
||||
self.service_auth = requests.auth.HTTPBasicAuth(email, token)
|
||||
self.url = url
|
||||
super().__init__()
|
||||
|
||||
def report(self):
|
||||
report_json = json.dumps(self._gen_report())
|
||||
rc = requests.post(self.url, auth=self.service_auth, data=report_json)
|
||||
if rc.status_code != 200:
|
||||
log.error("POST request to OpenObserve failed: %s", str(rc))
|
||||
|
||||
async def rcpc_conn_hdlr(websocket: ServerConnection):
|
||||
"""
|
||||
In this handler function we process the request from the the RCP Client. Before we perform any action we check if
|
||||
the rate limit is not exceeded. Then we describe the available commands to the client and execute the procedure
|
||||
the client asks for. When everything is done we close the connection normally. The client may skip executing any
|
||||
procedure by closing the connection early on his behalf.
|
||||
|
||||
The interaction with the client is recorded using a "flight recorder" object. When the interaction is done, the
|
||||
records are analyzed and a report is generated and sent to the OpenObserve monitoring entity.
|
||||
"""
|
||||
|
||||
# Immediately close the connection in case the rate limit has been exceeded.
|
||||
if rate_limiter.limit(websocket.remote_address[0]):
|
||||
await websocket.close(code=1008) # Policy Violation
|
||||
|
||||
# Create flight-recorder object
|
||||
flight_recorder = None
|
||||
if runtime_state.open_observe_pars:
|
||||
flight_recorder = OpenObserveFlightRecorder(**runtime_state.open_observe_pars)
|
||||
|
||||
# Execute procedure
|
||||
try:
|
||||
json_validator = JsonValidator(runtime_state.rcpc_to_rcps_schema, runtime_state.rcps_to_rcpc_schema)
|
||||
hdlr = RcpcSrvConnHdlr(websocket, CLIENT_TIMEOUT, json_validator, flight_recorder)
|
||||
await hdlr.check_version()
|
||||
await hdlr.describe()
|
||||
await hdlr.procedure()
|
||||
await hdlr.close()
|
||||
except Exception as e:
|
||||
backtrace("RCPC connection handler")
|
||||
if flight_recorder:
|
||||
flight_recorder.record_backtrace()
|
||||
flight_recorder.crash_report()
|
||||
await websocket.close(code=1011) # Internal Error
|
||||
|
||||
# Generate report from flight-recorder
|
||||
if flight_recorder:
|
||||
flight_recorder.report()
|
||||
|
||||
async def rcpm_conn_hdlr(websocket: ServerConnection):
|
||||
"""
|
||||
In this handler function we process requests from the RCP Module. We receive the description from the RCP Module.
|
||||
We keep the connection open throughout the whole lifetime of the RCP Module process so that we can know when the
|
||||
RCP Module becomes unavailable for some reason.
|
||||
"""
|
||||
try:
|
||||
json_validator = JsonValidator(runtime_state.rcpm_to_rcps_schema, runtime_state.rcps_to_rcpm_schema)
|
||||
hdlr = RcpmSrvConnHdlr(websocket, CLIENT_TIMEOUT, json_validator)
|
||||
await hdlr.check_version()
|
||||
await hdlr.describe()
|
||||
await hdlr.close()
|
||||
except:
|
||||
backtrace("RCPM connection handler")
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = option_parser.parse_args()
|
||||
PySimLogger.setup(print, {logging.WARN: "\033[33m", logging.DEBUG: "\033[90m"}, opts.verbose)
|
||||
|
||||
# Load SSL/TLS certificates
|
||||
rcpc_ssl_context = load_server_cert("RCP Client Server", opts.rcpc_server_cert)
|
||||
rcpm_ssl_context = load_server_cert("RCP Module Server", opts.rcpm_server_cert)
|
||||
rcpm_ca_ssl_context = load_ca_cert("RCP Module Command Server Client", opts.rcpm_module_ca_cert)
|
||||
|
||||
# Init card key provider for automatic card key retrieval
|
||||
card_key_provider_init(opts)
|
||||
|
||||
# Prepare parameters for OpenObserve
|
||||
if opts.open_observe_url and opts.open_observe_email and opts.open_observe_token:
|
||||
open_observe_pars = {'url' : opts.open_observe_url,
|
||||
'email': opts.open_observe_email,
|
||||
'token' : opts.open_observe_token}
|
||||
log.info("Reporting to OpenObserve: %s", open_observe_pars['url'])
|
||||
else:
|
||||
log.warning("Reporting to OpenObserve: (disabled)")
|
||||
open_observe_pars = None
|
||||
|
||||
# Start RCP server
|
||||
runtime_state = RuntimeState(rcpm_ca_ssl_context, open_observe_pars)
|
||||
rate_limiter = RateLimiter(interval=60, requests=opts.rcpc_request_limit)
|
||||
async def rcp_server():
|
||||
log.info("RCP Client Server at: %s:%d" % (opts.rcpc_server_addr, opts.rcpc_server_port))
|
||||
log.info("RCP Module server at: %s:%d" % (opts.rcpm_server_addr, opts.rcpm_server_port))
|
||||
async with serve(rcpc_conn_hdlr, opts.rcpc_server_addr, opts.rcpc_server_port, ssl=rcpc_ssl_context), \
|
||||
serve(rcpm_conn_hdlr, opts.rcpm_server_addr, opts.rcpm_server_port, ssl=rcpm_ssl_context):
|
||||
await asyncio.get_running_loop().create_future()
|
||||
try:
|
||||
asyncio.run(rcp_server())
|
||||
except SystemExit:
|
||||
pass
|
||||
except:
|
||||
backtrace("RCP Server")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Philipp Maier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU 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 ssl
|
||||
import json
|
||||
import abc
|
||||
import asyncio
|
||||
import time
|
||||
import websockets
|
||||
import traceback
|
||||
import threading
|
||||
from copy import deepcopy
|
||||
from websockets.asyncio.server import ServerConnection
|
||||
from websockets.asyncio.client import ClientConnection
|
||||
from pathlib import Path
|
||||
from jsonschema import validate
|
||||
from pySim.log import PySimLogger
|
||||
from ssl import SSLContext
|
||||
|
||||
log = PySimLogger.get(Path(__file__).stem)
|
||||
|
||||
def backtrace(what: str):
|
||||
log.error("%s failed with an exception:", what)
|
||||
log.error("---------------------8<---------------------")
|
||||
traceback_lines = traceback.format_exc()
|
||||
for line in traceback_lines.split("\n"):
|
||||
if line:
|
||||
log.error(line)
|
||||
log.error("---------------------8<---------------------")
|
||||
|
||||
def key_value_pairs_from_dict(keys: dict, keylabel: str='key', valuelabel: str='value') -> list:
|
||||
key_list = []
|
||||
for key in keys:
|
||||
key_list.append({keylabel : key, valuelabel : keys[key]})
|
||||
return key_list
|
||||
|
||||
def dict_from_key_value_pairs(keys: list, keylabel: str='key', valuelabel: str='value') -> dict:
|
||||
key_dict = {}
|
||||
for key in keys:
|
||||
key_dict[key[keylabel]] = key[valuelabel]
|
||||
return key_dict
|
||||
|
||||
def pytype_to_type(dict_in: dict) -> dict:
|
||||
"""
|
||||
There is no way to properly express python types in JSON. This function can be used to replace
|
||||
each occurrence of "pytype", with "type", where the string type name is replaced with an actual
|
||||
python type.
|
||||
"""
|
||||
dict_out = deepcopy(dict_in)
|
||||
if dict_out.get('pytype'):
|
||||
if dict_out['pytype'] == "str":
|
||||
dict_out.pop('pytype')
|
||||
dict_out['type'] = str
|
||||
elif dict_out['pytype'] == "int":
|
||||
dict_out.pop('pytype')
|
||||
dict_out['type'] = int
|
||||
else:
|
||||
raise ValueError("invalid type in command argument specification: %s" % arg['spec']['type'])
|
||||
return dict_out
|
||||
|
||||
def load_json_schema(filename: str) -> dict:
|
||||
"""Load a JSON schema from file"""
|
||||
log.debug("loading JSON schema: %s", filename)
|
||||
try:
|
||||
with open(filename) as schema_file:
|
||||
return json.load(schema_file)
|
||||
except Exception as e:
|
||||
backtrace("JSON schema load")
|
||||
sys.exit(1)
|
||||
|
||||
def load_server_cert(what: str, filename: str) -> SSLContext:
|
||||
"""Load an SSL/TLS server certificate"""
|
||||
log.debug("loading SSL/TLS server certificate (%s): %s", what, filename)
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_cert_chain(filename)
|
||||
return ssl_context
|
||||
|
||||
def load_ca_cert(what: str, filename: str) -> SSLContext:
|
||||
"""Load an SSL/TLS CA certificate"""
|
||||
log.info("loading SSL/TLS CA certificate (%s): %s", what, filename)
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ssl_context.load_verify_locations(filename)
|
||||
return ssl_context
|
||||
|
||||
class JsonValidator():
|
||||
"""
|
||||
JSON validator class, can be passed to any ConnHdlr object to automatically validate the JSON messages which are
|
||||
sent and and received.
|
||||
"""
|
||||
|
||||
def __init__(self, rx_schema: dict, tx_schema: dict = None):
|
||||
self.rx_schema = rx_schema
|
||||
if tx_schema:
|
||||
self.tx_schema = tx_schema
|
||||
else:
|
||||
self.tx_schema = None
|
||||
|
||||
def valid_rx_json(self, rx_json: dict):
|
||||
validate(instance = rx_json, schema = self.rx_schema)
|
||||
|
||||
def valid_tx_json(self, tx_json: dict):
|
||||
if self.tx_schema:
|
||||
# We intentionally do not prevent the sending of an invalid JSON message. It is the responsibility of the
|
||||
# receiving end to detect an invalid message and react accordingly. The purpose of this validation is to
|
||||
# make developers/users aware of the problem.
|
||||
try:
|
||||
validate(instance = tx_json, schema = self.tx_schema)
|
||||
except Exception as e:
|
||||
backtrace("JSON schema validation (TX)")
|
||||
|
||||
class FlightRecorder(abc.ABC):
|
||||
"""
|
||||
Base class to create a FlightRecorder object which can be passed to any ConnHdlr object to record debug information
|
||||
(record_comm, record_debug) and metadata (record_meta) throughout the lifetime of a ConnHdlr object. In case the
|
||||
ConnHdlr throws an exception, the API user may call the record_backtrace method to record a backtrace. Finally,
|
||||
the APU user may call the report method (which calls _gen_report internally) to send a report to an external
|
||||
monitoring enitiy.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.records_meta = {}
|
||||
self.records_comm = []
|
||||
self.records_debug = []
|
||||
self.crash_report_flag = False
|
||||
self.record_meta('timestamp_start', time.strftime('%Y-%m-%d %H:%M:%S'))
|
||||
|
||||
def record_meta(self, key: str, value):
|
||||
"""Record/Update metadata"""
|
||||
self.records_meta[key] = value
|
||||
|
||||
def record_comm(self, key: str, value):
|
||||
"""Record communication (automatically called by the ConnHdlr object)"""
|
||||
self.records_comm.append({key : value})
|
||||
|
||||
def record_debug(self, key: str, value):
|
||||
"""Record debug information"""
|
||||
self.records_debug.append({key : value})
|
||||
|
||||
def record_backtrace(self):
|
||||
"""Record a backtrace"""
|
||||
traceback_lines = traceback.format_exc()
|
||||
traceback_lines_filtered = []
|
||||
for line in traceback_lines.split("\n"):
|
||||
if line:
|
||||
traceback_lines_filtered.append(line)
|
||||
self.record_debug('backtrace', traceback_lines_filtered)
|
||||
|
||||
def crash_report(self):
|
||||
"""Set crash_report_flag. Thie method shall be called if an unrecoverable error has occurred."""
|
||||
self.crash_report_flag = True
|
||||
|
||||
def _gen_report(self):
|
||||
"""
|
||||
Generate a report from the collected data. In case the crash_report flag is set to true, the report will
|
||||
include communications (records_comm) and debug information (records_debug). Otherwise only the metadata
|
||||
(records_meta) will be included.
|
||||
"""
|
||||
self.record_meta('timestamp_end', time.strftime('%Y-%m-%d %H:%M:%S'))
|
||||
report = self.records_meta
|
||||
if self.crash_report_flag:
|
||||
report['comm'] = self.records_comm
|
||||
report['debug'] = self.records_debug
|
||||
report['report_type'] = 'crash'
|
||||
log.warning("crash report: %s", str(report))
|
||||
return report
|
||||
else:
|
||||
report['report_type'] = 'normal'
|
||||
log.debug("normal report: %s", str(report))
|
||||
return report
|
||||
|
||||
@abc.abstractmethod
|
||||
def report(self):
|
||||
"""
|
||||
To be implemented in the derived class. Shall call _gen_report and then send the report to an external
|
||||
monitoring entity.
|
||||
"""
|
||||
pass
|
||||
|
||||
class ConnHdlr(abc.ABC):
|
||||
"""Base class that can be used to create a connection handler"""
|
||||
|
||||
def __init__(self, websocket: ServerConnection | ClientConnection, timeout: int,
|
||||
json_validator: JsonValidator = None, flight_recorder: FlightRecorder = None):
|
||||
self.websocket = websocket
|
||||
self.local_address = websocket.local_address
|
||||
self.remote_address = websocket.remote_address
|
||||
self.timeout = timeout
|
||||
self.json_validator = json_validator
|
||||
self.flight_recorder = flight_recorder
|
||||
log.debug(str(self) + " -- new handler, timeout: %d sec.", self.timeout)
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_meta(type(self).__name__ + '_remote_address',
|
||||
str(self.remote_address[0]) + ":" + str(self.remote_address[1]))
|
||||
self.flight_recorder.record_meta(type(self).__name__ + '_timestamp', time.strftime('%Y-%m-%d %H:%M:%S'))
|
||||
self.flight_recorder.record_meta(type(self).__name__ + '_id', id(self))
|
||||
|
||||
def _log_recv_peer(self, rx_json_str: str):
|
||||
peer = "%s:%d<-%s:%d" % (self.local_address[0],
|
||||
self.local_address[1],
|
||||
self.remote_address[0],
|
||||
self.remote_address[1])
|
||||
log.debug(str(self) + " -- RX(%s): %s", peer, rx_json_str)
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_comm(type(self).__name__ + '_rx', rx_json_str)
|
||||
|
||||
def _log_send_peer(self, tx_json_str: str):
|
||||
peer = "%s:%d->%s:%d" % (self.local_address[0],
|
||||
self.local_address[1],
|
||||
self.remote_address[0],
|
||||
self.remote_address[1])
|
||||
log.debug(str(self) + " -- TX(%s): %s", peer, tx_json_str)
|
||||
if self.flight_recorder:
|
||||
self.flight_recorder.record_comm(type(self).__name__ + '_tx', tx_json_str)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s(%d)" % (type(self).__name__, id(self))
|
||||
|
||||
def __del__(self):
|
||||
log.debug(str(self) + " -- closed handler")
|
||||
|
||||
class SrvConnHdlr(ConnHdlr):
|
||||
"""Base class that can be used to create a connection handler for a server"""
|
||||
|
||||
async def _recv(self) -> dict:
|
||||
"""Receive JSON message from client"""
|
||||
async with asyncio.timeout(self.timeout):
|
||||
try:
|
||||
rx_json_str = await self.websocket.recv()
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
log.debug(str(self) + " -- no data received, connection is closed")
|
||||
return None
|
||||
self._log_recv_peer(rx_json_str)
|
||||
rx_json = json.loads(rx_json_str)
|
||||
if self.json_validator:
|
||||
self.json_validator.valid_rx_json(rx_json)
|
||||
return rx_json
|
||||
|
||||
async def _send(self, tx_json: dict):
|
||||
"""Send JSON message to client"""
|
||||
if self.json_validator:
|
||||
self.json_validator.valid_tx_json(tx_json)
|
||||
tx_json_str = json.dumps(tx_json)
|
||||
self._log_send_peer(tx_json_str)
|
||||
await self.websocket.send(tx_json_str)
|
||||
|
||||
async def _transact(self, tx_json: dict) -> dict:
|
||||
"""Exchange JSON message with client"""
|
||||
await self._send(tx_json)
|
||||
return await self._recv()
|
||||
|
||||
async def close(self):
|
||||
"""Wait for a connecion to close normally"""
|
||||
await self.websocket.wait_closed()
|
||||
log.debug(str(self) + " -- closed connection")
|
||||
|
||||
class SrvSyncConnHdlr(ConnHdlr):
|
||||
"""Base class that can be used to create a synchronous connection handler for a server"""
|
||||
|
||||
def _recv(self) -> dict:
|
||||
"""Receive JSON message from client"""
|
||||
rx_json_str = self.websocket.recv(self.timeout)
|
||||
self._log_recv_peer(rx_json_str)
|
||||
rx_json = json.loads(rx_json_str)
|
||||
if self.json_validator:
|
||||
self.json_validator.valid_rx_json(rx_json)
|
||||
return rx_json
|
||||
|
||||
def _send(self, tx_json: dict):
|
||||
"""Send JSON message to client"""
|
||||
if self.json_validator:
|
||||
self.json_validator.valid_tx_json(tx_json)
|
||||
tx_json_str = json.dumps(tx_json)
|
||||
self._log_send_peer(tx_json_str)
|
||||
self.websocket.send(tx_json_str)
|
||||
|
||||
def _transact(self, tx_json: dict) -> dict:
|
||||
"""Exchange JSON message with client"""
|
||||
self._send(tx_json)
|
||||
return self._recv()
|
||||
|
||||
def close(self):
|
||||
"""Close connection normally"""
|
||||
self.websocket.close()
|
||||
log.debug(str(self) + " -- closed connection")
|
||||
|
||||
class CltConnHdlr(ConnHdlr):
|
||||
"""Base class that can be used to create a connection handler for a client"""
|
||||
|
||||
async def _transact(self, tx_json: dict) -> dict:
|
||||
"""Exchange JSON message with server"""
|
||||
if self.json_validator:
|
||||
self.json_validator.valid_tx_json(tx_json)
|
||||
tx_json_str = json.dumps(tx_json)
|
||||
self._log_send_peer(tx_json_str)
|
||||
async with asyncio.timeout(self.timeout):
|
||||
await self.websocket.send(tx_json_str)
|
||||
rx_json_str = await self.websocket.recv()
|
||||
self._log_recv_peer(rx_json_str)
|
||||
rx_json = json.loads(rx_json_str);
|
||||
if self.json_validator:
|
||||
self.json_validator.valid_rx_json(rx_json)
|
||||
return rx_json
|
||||
|
||||
async def close(self):
|
||||
"""Close connection normally"""
|
||||
await self.websocket.close()
|
||||
log.debug(str(self) + " -- closed connection")
|
||||
|
||||
async def wait_close(self):
|
||||
"""Wait for a connecion to close normally"""
|
||||
await self.websocket.wait_closed()
|
||||
log.debug(str(self) + " -- closed connection")
|
||||
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "RCP Client to RCP Server",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rcpc_version": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"software": {
|
||||
"type": "string"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [ "software", "protocol" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcpc_hello": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"suitable_for": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"atr": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,66}$"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "atr" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [ "suitable_for" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcpc_command": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cmd": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9,A-Z,a-z,_]{0,40}$"
|
||||
},
|
||||
"cmd_argv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^.{0,512}$"
|
||||
},
|
||||
"maxItems": 255
|
||||
}
|
||||
},
|
||||
"required": [ "cmd", "cmd_argv" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcpc_result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"r_apdu": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,512}$"
|
||||
},
|
||||
"sw": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,4}$"
|
||||
}
|
||||
},
|
||||
"required": [ "data", "sw" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"atr": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,66}$"
|
||||
},
|
||||
"empty": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "r_apdu" ] },
|
||||
{ "required": [ "atr" ] },
|
||||
{ "required": [ "empty" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "rcpc_version" ] },
|
||||
{ "required": [ "rcpc_hello" ] },
|
||||
{ "required": [ "rcpc_command" ] },
|
||||
{ "required": [ "rcpc_result" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "RCP Module to RCP Server",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rcpm_version": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"protocol": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [ "protocol" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcpm_hello": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"cmd_descr": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"required" : {
|
||||
"type": "boolean"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
},
|
||||
"action": {
|
||||
"type": "string"
|
||||
},
|
||||
"pytype": {
|
||||
"type": "string"
|
||||
},
|
||||
"default" : {
|
||||
"type": ["string", "integer"]
|
||||
}
|
||||
},
|
||||
"required": [ "help" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [ "name", "spec" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"get_keys": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uicc" : {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"euicc" : {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "uicc" ] },
|
||||
{ "required": [ "euicc" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [ "name", "help", "args" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"suitable_for": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"atr": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,66}$"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "atr" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"addr": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [ "name", "cmd_descr", "suitable_for", "addr", "port" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "rcpm_hello" ] },
|
||||
{ "required": [ "rcpm_version" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "RCP Module Command Server to RCP Server",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rcps_instr": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"print": {
|
||||
"type": "string"
|
||||
},
|
||||
"reset": {
|
||||
"type": "null"
|
||||
},
|
||||
"c_apdu": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,512}$"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "print" ] },
|
||||
{ "required": [ "reset" ] },
|
||||
{ "required": [ "c_apdu" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcps_goodbye": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "rcps_instr" ] },
|
||||
{ "required": [ "rcps_goodbye" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "RCP Server to RCP Client",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rcpc_version": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"software": {
|
||||
"type": "string"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string"
|
||||
},
|
||||
"info": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [ "software", "protocol" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcpc_welcome": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"module_descr": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"cmd_descr": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"required" : {
|
||||
"type": "boolean"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
},
|
||||
"action": {
|
||||
"type": "string"
|
||||
},
|
||||
"pytype": {
|
||||
"type": "string"
|
||||
},
|
||||
"default" : {
|
||||
"type": ["string", "integer"]
|
||||
}
|
||||
},
|
||||
"required": [ "help" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [ "name", "spec" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "name", "help", "args" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "name", "cmd_descr" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "module_descr" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcpc_instr": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"print": {
|
||||
"type": "string"
|
||||
},
|
||||
"reset": {
|
||||
"type": "null"
|
||||
},
|
||||
"c_apdu": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,512}$"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "print" ] },
|
||||
{ "required": [ "reset" ] },
|
||||
{ "required": [ "c_apdu" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcpc_goodbye": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "rcpc_version" ] },
|
||||
{ "required": [ "rcpc_welcome" ] },
|
||||
{ "required": [ "rcpc_instr" ] },
|
||||
{ "required": [ "rcpc_goodbye" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "RCP Server to RCP Module",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rcpm_version": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"protocol": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [ "protocol" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcpm_welcome": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "rcpm_version" ] },
|
||||
{ "required": [ "rcpm_welcome" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "RCP Server to RCP Module Command Server",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rcps_command": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cmd": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9,A-Z,a-z,_]{0,40}$"
|
||||
},
|
||||
"cmd_argv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^.{0,512}$"
|
||||
},
|
||||
"maxItems": 255
|
||||
},
|
||||
"keys" : {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uicc" : {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [ "key", "value" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"euicc" : {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [ "key", "value" ],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "uicc" ] },
|
||||
{ "required": [ "euicc" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [ "cmd", "cmd_argv" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rcps_result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"r_apdu": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,512}$"
|
||||
},
|
||||
"sw": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,4}$"
|
||||
}
|
||||
},
|
||||
"required": [ "data", "sw" ],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"atr": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9A-F]{0,66}$"
|
||||
},
|
||||
"empty": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "r_apdu" ] },
|
||||
{ "required": [ "atr" ] },
|
||||
{ "required": [ "empty" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": [ "rcps_command" ] },
|
||||
{ "required": [ "rcps_result" ] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
iccid,kic,kid,kik
|
||||
8949440000001155306,F09C43EE1A0391665CC9F05AF4E0BD10,01981F4A20999F62AF99988007BAF6CA,8F8AEE5CDCC5D361368BC45673D99195
|
||||
|
@@ -1,2 +0,0 @@
|
||||
"ICCID","KIC","KID","KIK"
|
||||
"8949440000001155306","eae46224fa0a4ac1c12cba9d102f1188","3f14b978ddb38c08d832d4e4c2e0639d","9e19db4a5ed5cb8c4f5d96283eab273a"
|
||||
@@ -1,20 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDSzCCAjOgAwIBAgIUEv1f0yjVtkr+RNYLItZ33eTJwHMwDQYJKoZIhvcNAQEL
|
||||
BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjYwNDI5MTIwOTM0WhcNMzYw
|
||||
NDI2MTIwOTM0WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBANXdkSyQlDzuo2cJmnBmFiZpc0V9tYBcNkpZd3Ac
|
||||
R0WljazKKgXDWNmOcSO7891bi+1HZzz+nDfV0mJY776ScGkTqF43Hzpg9eZakMAx
|
||||
yC24mT4h+uyRcPWZrBwaQhpiQrvZy4MRyuUB+BEgBSmhoDiuXP44kWiuEJHuzpOq
|
||||
X6Q2dW8RIeQPDGGK6XPZIQLqx+krxkaqphd/vHgT1/yd7Ol5xxMc4x2UuPaVCj0D
|
||||
OzslFsbb0Zu77ffCtHOVVnzSCzeEGGx1MPQm6hDVW+KUXXTwke1K55fmFZhu0gKO
|
||||
HYSEjgPj6X8muDb+GvOAQX3fHmS6KvFS4fwWd2InZ3v2f3cCAwEAAaOBkDCBjTAM
|
||||
BgNVHRMEBTADAQH/MB0GA1UdDgQWBBS6zY4Dd0pJFrvWLmyjn0vDTFqVqzBRBgNV
|
||||
HSMESjBIgBS6zY4Dd0pJFrvWLmyjn0vDTFqVq6EapBgwFjEUMBIGA1UEAwwLRWFz
|
||||
eS1SU0EgQ0GCFBL9X9Mo1bZK/kTWCyLWd93kycBzMAsGA1UdDwQEAwIBBjANBgkq
|
||||
hkiG9w0BAQsFAAOCAQEAGJUXlbnVhh+xL+pyTyjwtd8nxhUcHzYZl+OT0bkGY9zT
|
||||
S3NjHkKBbdnEftuYDYqp0uBuGFQ1WIOKiM3rp4IePKe84lSivZMVh9ObtNalcEQr
|
||||
sqxBziNOMJM2mh5V2NdxiK2E1gCZ959wOQ8yzM6gGC+wW8w4zwULhv4JimQDjk+G
|
||||
kAdiGL7+WAxrNWUulvm8khFt2nOlucJg4IAYVt2SI1AFMt/YSXoA4wMwM9QcHGj0
|
||||
1A069IxX93WVhUpIL1Avwz+KJK0BPY6SM8LYUy6V50Hojp76BB7VD6SxQrSoceUo
|
||||
6cRNDtCmofOlltfeUJLr1mI4S2tM50bQVsHD92EJBA==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,115 +0,0 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number:
|
||||
42:38:a5:6f:70:53:40:e4:a4:1a:2c:0f:fc:81:13:42
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: CN=Easy-RSA CA
|
||||
Validity
|
||||
Not Before: Apr 29 12:09:35 2026 GMT
|
||||
Not After : Aug 1 12:09:35 2028 GMT
|
||||
Subject: CN=example_ssl_rcpc_rcps_cert
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (2048 bit)
|
||||
Modulus:
|
||||
00:ae:0f:e1:ee:fc:f6:db:75:45:c0:f4:49:72:46:
|
||||
3d:e3:db:0c:c4:34:d2:9e:49:d4:86:4f:19:0d:55:
|
||||
70:50:81:e4:e6:64:56:a8:58:e8:e6:54:0a:16:bc:
|
||||
f4:4b:84:cd:1d:b9:2e:ed:62:b6:cd:62:35:8b:81:
|
||||
18:ab:ff:63:f5:c1:dc:16:3e:a8:dc:ac:11:dd:43:
|
||||
12:f8:ef:f2:f1:af:84:fd:83:fe:a8:d3:46:7d:77:
|
||||
e6:ae:95:61:a6:c9:99:6b:40:61:8d:6e:7e:66:1e:
|
||||
97:77:b0:e8:b7:3d:3a:d5:d7:d3:ee:66:95:62:83:
|
||||
14:cc:5e:32:ff:9e:bd:f1:06:e6:8d:6a:7c:0a:27:
|
||||
22:19:b9:06:09:cf:ef:c7:dc:e8:8f:04:4b:83:0d:
|
||||
cc:8d:b1:c2:cf:ab:40:25:6e:f2:bf:b7:c6:1d:8f:
|
||||
d2:fc:3d:c8:a1:be:4a:09:b9:91:e3:76:4f:c7:9b:
|
||||
fc:2f:de:d9:bb:eb:df:d3:d8:8c:72:79:bd:bf:10:
|
||||
8b:01:e6:0f:7f:bb:f6:75:31:5a:40:ad:df:e1:07:
|
||||
e6:12:12:b2:d3:99:d0:bd:24:5a:9a:ce:62:4f:da:
|
||||
fe:0d:df:09:ae:da:04:83:54:e8:cb:68:c0:57:78:
|
||||
c2:f4:68:42:d7:f4:81:4a:a3:b4:4e:0b:49:95:26:
|
||||
1d:15
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
X509v3 Subject Key Identifier:
|
||||
8E:99:9D:C0:70:98:57:16:08:8E:DF:6E:51:78:A6:86:18:FF:06:52
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB
|
||||
DirName:/CN=Easy-RSA CA
|
||||
serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73
|
||||
X509v3 Extended Key Usage:
|
||||
TLS Web Server Authentication
|
||||
X509v3 Key Usage:
|
||||
Digital Signature, Key Encipherment
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:127.0.0.1, IP Address:127.0.0.1
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Signature Value:
|
||||
3e:56:20:f9:3b:fa:13:6e:7e:a9:80:a6:15:18:01:82:f1:b8:
|
||||
4d:1b:f1:ee:da:ed:50:f7:3b:13:01:a5:14:f9:4c:0e:34:57:
|
||||
dc:e6:d1:7e:02:30:af:3b:fd:c9:ae:18:16:c9:3b:0a:4e:20:
|
||||
da:cd:e8:cc:05:0c:b3:7d:6f:e5:15:ff:66:59:6b:fe:ff:1a:
|
||||
ef:ca:b5:3a:1a:ad:dd:f6:19:43:d9:2b:61:18:29:95:b4:0c:
|
||||
1e:b2:4a:ce:80:d3:1b:59:dc:62:ec:50:21:37:9c:2f:7a:4d:
|
||||
c2:ac:de:1b:1d:a3:25:e0:e8:33:42:cf:77:31:2a:f2:44:36:
|
||||
ef:59:89:da:6c:3e:9a:e8:d7:06:39:17:d5:78:82:6d:b6:63:
|
||||
3f:9a:40:3b:e6:12:58:52:3d:63:4e:85:0b:02:cb:40:d2:8a:
|
||||
59:8d:8f:ee:4a:c8:97:91:51:a9:2f:1b:15:81:9c:20:dd:94:
|
||||
08:6f:ac:fa:c6:28:90:6c:17:5a:23:87:9a:5b:e5:c6:2e:f3:
|
||||
09:66:de:76:1b:60:42:c1:5c:71:88:87:f6:7b:cb:e3:7e:14:
|
||||
67:c9:a0:15:98:b6:7b:75:40:9a:08:fc:77:39:3a:23:cb:e3:
|
||||
78:7d:57:f9:a7:66:36:b4:b5:07:de:61:3a:dd:07:58:b3:4f:
|
||||
41:f6:f4:d9
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDhDCCAmygAwIBAgIQQjilb3BTQOSkGiwP/IETQjANBgkqhkiG9w0BAQsFADAW
|
||||
MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yNjA0MjkxMjA5MzVaFw0yODA4MDEx
|
||||
MjA5MzVaMCUxIzAhBgNVBAMMGmV4YW1wbGVfc3NsX3JjcGNfcmNwc19jZXJ0MIIB
|
||||
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArg/h7vz223VFwPRJckY949sM
|
||||
xDTSnknUhk8ZDVVwUIHk5mRWqFjo5lQKFrz0S4TNHbku7WK2zWI1i4EYq/9j9cHc
|
||||
Fj6o3KwR3UMS+O/y8a+E/YP+qNNGfXfmrpVhpsmZa0BhjW5+Zh6Xd7Dotz061dfT
|
||||
7maVYoMUzF4y/5698QbmjWp8CiciGbkGCc/vx9zojwRLgw3MjbHCz6tAJW7yv7fG
|
||||
HY/S/D3Iob5KCbmR43ZPx5v8L97Zu+vf09iMcnm9vxCLAeYPf7v2dTFaQK3f4Qfm
|
||||
EhKy05nQvSRams5iT9r+Dd8JrtoEg1Toy2jAV3jC9GhC1/SBSqO0TgtJlSYdFQID
|
||||
AQABo4G+MIG7MAkGA1UdEwQCMAAwHQYDVR0OBBYEFI6ZncBwmFcWCI7fblF4poYY
|
||||
/wZSMFEGA1UdIwRKMEiAFLrNjgN3SkkWu9YubKOfS8NMWpWroRqkGDAWMRQwEgYD
|
||||
VQQDDAtFYXN5LVJTQSBDQYIUEv1f0yjVtkr+RNYLItZ33eTJwHMwEwYDVR0lBAww
|
||||
CgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMBoGA1UdEQQTMBGCCTEyNy4wLjAuMYcE
|
||||
fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAPlYg+Tv6E25+qYCmFRgBgvG4TRvx7trt
|
||||
UPc7EwGlFPlMDjRX3ObRfgIwrzv9ya4YFsk7Ck4g2s3ozAUMs31v5RX/Zllr/v8a
|
||||
78q1Ohqt3fYZQ9krYRgplbQMHrJKzoDTG1ncYuxQITecL3pNwqzeGx2jJeDoM0LP
|
||||
dzEq8kQ271mJ2mw+mujXBjkX1XiCbbZjP5pAO+YSWFI9Y06FCwLLQNKKWY2P7krI
|
||||
l5FRqS8bFYGcIN2UCG+s+sYokGwXWiOHmlvlxi7zCWbedhtgQsFccYiH9nvL434U
|
||||
Z8mgFZi2e3VAmgj8dzk6I8vjeH1X+admNrS1B95hOt0HWLNPQfb02Q==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuD+Hu/PbbdUXA
|
||||
9ElyRj3j2wzENNKeSdSGTxkNVXBQgeTmZFaoWOjmVAoWvPRLhM0duS7tYrbNYjWL
|
||||
gRir/2P1wdwWPqjcrBHdQxL47/Lxr4T9g/6o00Z9d+aulWGmyZlrQGGNbn5mHpd3
|
||||
sOi3PTrV19PuZpVigxTMXjL/nr3xBuaNanwKJyIZuQYJz+/H3OiPBEuDDcyNscLP
|
||||
q0AlbvK/t8Ydj9L8PcihvkoJuZHjdk/Hm/wv3tm769/T2Ixyeb2/EIsB5g9/u/Z1
|
||||
MVpArd/hB+YSErLTmdC9JFqazmJP2v4N3wmu2gSDVOjLaMBXeML0aELX9IFKo7RO
|
||||
C0mVJh0VAgMBAAECggEAGBhmQhdeE+Cu1Ihsn2dWW3PAF2wpiNR3GVWbRfOBHf/x
|
||||
QCx9K4ZNTU8ua1niZo7edyIiuyVaYWGaQHLRR8QNoiBhN2oapZujSHInzvKmeqr9
|
||||
ubt7NgMzQ5ykwB+5OiW3uXda2cGFOV08QgspF+6ftakQMzUbslyrdSQIIscmi5Ya
|
||||
uTDvE6+lBFinxy3RHFKVCZ3UrsDwfHR4eTUmgCHRB27joB7DFXL32amv0M8HjoGz
|
||||
EZKGJgTwmRf9U4z4D4wCnOfVAPlsuthKUqMuTlBg0ZEstMZrzlP4suT2ieku0Usv
|
||||
0XbJ38VozPYYFdR7nApVVvrJgHzI9cpoUbGto4BLOQKBgQDbXjFVLffOec8hv9dN
|
||||
2VGZQmK61S9OrbvTnEOlxJd+kRid71X1pV5TuPKJJQtUJXf429bQOs40YbLeOmJt
|
||||
BiRSR5yIBH7hDDC/c0ynqunstwDlgz+QX2Oh2B4alvVaWy0rZYF6NpBiI0+R5r5V
|
||||
C2fHRS4LLPoflg83+CMubyLS6QKBgQDLIOXxlp1JQTzXhJkrkytLkafmEHAafovt
|
||||
wbRD50/s+dl16BRX12sK0gXj2vwu0FleUD6Z7afDfspmvQdg3fyDxYw9Q+vw5LYQ
|
||||
7BvoVU99o1m468yXwX/v36peCt4nOpwkJZKJfjgxjnMJByyeSUgL9uW4K+0D0LBV
|
||||
a5Iv7QklTQKBgH30BkVPIHKIE/rfyIJlXemuaTu2/fOh4y9sEJdUWluMeeLssaFa
|
||||
ct+FWJSQFYIaBVl4+E0VBqKi2e2o/ix1E1O+1ExwsF0M/8xdKk024BtPNA+TnWKK
|
||||
so0Rpq9Dr9pScYvyOzZtr9b5SU2PfAcehlavDPHTwEV0hoZvTdvyab9JAoGADMBJ
|
||||
7vp3cSvJN/Y470VTyHCiS4zonKEpA4nPWRviJowgnIgvDryVGZ7Jg94xSncFxSfg
|
||||
ZiVHDLye1Ag1uFz3BwaVoRrsarjQvQs1TUZdsRNaBIO42iXpdBNkTHb+LxQ8zQAW
|
||||
zM7BlErO6dgrctxCy416Ki+Ht1+YUiRojt2gX1kCgYBqytUy+XkPi5j3Ga29xcvP
|
||||
WI3Uc8RI2GmoAmrw5QFiSG6lNXAzfo2ZNQbFnxgxeMOG9fV9yzBdIjXWNWr0E/KH
|
||||
Fsb65R8iIrXQB9BZjuQqjz9nDm7eZZUBNGGbQ4DgSepnp194gXC5DoAElzuwOXbE
|
||||
pY/kM1KwlpUR3J3LeF3i+Q==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,115 +0,0 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number:
|
||||
e7:09:ab:70:b5:dc:1f:11:d9:2a:23:04:39:87:34:f3
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: CN=Easy-RSA CA
|
||||
Validity
|
||||
Not Before: Apr 29 12:09:35 2026 GMT
|
||||
Not After : Aug 1 12:09:35 2028 GMT
|
||||
Subject: CN=example_ssl_rcpm_rcps_cert
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (2048 bit)
|
||||
Modulus:
|
||||
00:cc:79:9b:d3:f3:1f:41:9f:00:48:cd:47:0b:ae:
|
||||
b9:1c:4e:3e:55:e2:4e:5f:a8:cc:13:d5:dd:bd:f0:
|
||||
01:4c:19:ae:e3:a9:09:06:89:92:49:f7:bb:90:28:
|
||||
fb:8c:22:69:b5:f5:a0:50:3d:97:0f:1e:1d:b1:a8:
|
||||
57:9b:d7:e2:0d:99:67:7f:02:82:0c:9c:8e:dd:13:
|
||||
03:28:93:b5:cb:7e:b5:78:06:10:bf:7b:55:c3:f7:
|
||||
10:8b:20:4a:1c:f9:f1:b2:fa:f1:c7:44:9d:0a:ce:
|
||||
ef:8d:f9:e8:ff:d1:c1:69:ec:8e:5f:11:cc:c9:98:
|
||||
d5:1c:33:e2:5b:7a:4d:34:dc:76:c3:cd:db:4c:93:
|
||||
d1:08:78:6f:3c:9a:ee:74:39:1e:cd:65:1e:c9:35:
|
||||
cc:3b:2b:9e:d7:49:10:8e:58:85:b0:10:5b:90:1e:
|
||||
f1:5e:d5:92:04:93:f9:33:c6:9d:77:63:d1:33:46:
|
||||
5b:98:ff:9a:a8:f5:df:f7:84:21:e2:88:28:7a:a4:
|
||||
c6:0d:9f:25:7e:0d:73:5b:d5:53:4a:90:79:94:37:
|
||||
14:f3:c8:75:76:d4:1c:32:51:bf:58:16:74:d5:8d:
|
||||
18:b6:53:f4:ab:cb:91:a8:8c:a3:ca:3c:5c:35:b6:
|
||||
5f:62:57:37:5a:75:28:b7:4d:26:aa:ea:50:da:a4:
|
||||
1c:55
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
X509v3 Subject Key Identifier:
|
||||
47:92:B5:81:8B:5C:14:98:B3:83:B6:EB:06:9F:43:F3:3A:7E:ED:24
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB
|
||||
DirName:/CN=Easy-RSA CA
|
||||
serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73
|
||||
X509v3 Extended Key Usage:
|
||||
TLS Web Server Authentication
|
||||
X509v3 Key Usage:
|
||||
Digital Signature, Key Encipherment
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:127.0.0.1, IP Address:127.0.0.1
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Signature Value:
|
||||
6d:31:e6:29:d2:3b:a8:90:5c:4b:ac:61:15:95:5d:70:66:a5:
|
||||
77:9d:88:47:49:73:75:be:70:69:d8:2f:62:82:5e:83:86:3b:
|
||||
a8:48:3f:f1:5f:22:ae:81:23:64:c4:f2:2b:dd:4d:be:e5:6a:
|
||||
26:a5:ea:c7:ba:1b:3e:6a:34:03:5a:f1:49:28:5f:56:4a:a6:
|
||||
0e:1b:7a:07:48:76:95:b6:4b:f5:3f:b9:67:2e:e0:33:06:80:
|
||||
d4:d6:01:a5:76:01:c0:a5:18:e5:38:8b:52:73:6e:6d:45:50:
|
||||
b7:9a:ab:86:5d:e3:65:b4:b8:c7:ee:b2:dc:bf:e3:d5:bb:e4:
|
||||
91:eb:f5:0c:38:22:5e:37:54:9e:ba:96:25:10:04:18:23:f7:
|
||||
ae:73:4d:d0:aa:03:81:b4:89:36:97:15:da:1a:60:a0:98:5f:
|
||||
03:f8:1b:22:83:57:41:4b:12:28:7d:8d:ea:88:74:24:28:5c:
|
||||
53:41:89:5e:9a:da:fd:7b:bf:60:dc:de:9b:49:ce:5c:a3:b2:
|
||||
01:7d:1d:cb:28:8c:ba:f4:7b:5d:2b:cb:15:5b:2a:97:1a:d1:
|
||||
f9:e7:12:e3:43:b9:f4:2a:88:dd:6d:b6:a0:72:d3:bd:63:23:
|
||||
e9:d7:f0:ac:b5:6d:0d:f2:d9:8b:2c:c4:35:5b:4d:83:dc:e8:
|
||||
7d:0b:3d:a3
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDhTCCAm2gAwIBAgIRAOcJq3C13B8R2SojBDmHNPMwDQYJKoZIhvcNAQELBQAw
|
||||
FjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjYwNDI5MTIwOTM1WhcNMjgwODAx
|
||||
MTIwOTM1WjAlMSMwIQYDVQQDDBpleGFtcGxlX3NzbF9yY3BtX3JjcHNfY2VydDCC
|
||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMx5m9PzH0GfAEjNRwuuuRxO
|
||||
PlXiTl+ozBPV3b3wAUwZruOpCQaJkkn3u5Ao+4wiabX1oFA9lw8eHbGoV5vX4g2Z
|
||||
Z38Cggycjt0TAyiTtct+tXgGEL97VcP3EIsgShz58bL68cdEnQrO74356P/RwWns
|
||||
jl8RzMmY1Rwz4lt6TTTcdsPN20yT0Qh4bzya7nQ5Hs1lHsk1zDsrntdJEI5YhbAQ
|
||||
W5Ae8V7VkgST+TPGnXdj0TNGW5j/mqj13/eEIeKIKHqkxg2fJX4Nc1vVU0qQeZQ3
|
||||
FPPIdXbUHDJRv1gWdNWNGLZT9KvLkaiMo8o8XDW2X2JXN1p1KLdNJqrqUNqkHFUC
|
||||
AwEAAaOBvjCBuzAJBgNVHRMEAjAAMB0GA1UdDgQWBBRHkrWBi1wUmLODtusGn0Pz
|
||||
On7tJDBRBgNVHSMESjBIgBS6zY4Dd0pJFrvWLmyjn0vDTFqVq6EapBgwFjEUMBIG
|
||||
A1UEAwwLRWFzeS1SU0EgQ0GCFBL9X9Mo1bZK/kTWCyLWd93kycBzMBMGA1UdJQQM
|
||||
MAoGCCsGAQUFBwMBMAsGA1UdDwQEAwIFoDAaBgNVHREEEzARggkxMjcuMC4wLjGH
|
||||
BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAG0x5inSO6iQXEusYRWVXXBmpXediEdJ
|
||||
c3W+cGnYL2KCXoOGO6hIP/FfIq6BI2TE8ivdTb7laial6se6Gz5qNANa8UkoX1ZK
|
||||
pg4begdIdpW2S/U/uWcu4DMGgNTWAaV2AcClGOU4i1Jzbm1FULeaq4Zd42W0uMfu
|
||||
sty/49W75JHr9Qw4Il43VJ66liUQBBgj965zTdCqA4G0iTaXFdoaYKCYXwP4GyKD
|
||||
V0FLEih9jeqIdCQoXFNBiV6a2v17v2Dc3ptJzlyjsgF9HcsojLr0e10ryxVbKpca
|
||||
0fnnEuNDufQqiN1ttqBy071jI+nX8Ky1bQ3y2YssxDVbTYPc6H0LPaM=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMeZvT8x9BnwBI
|
||||
zUcLrrkcTj5V4k5fqMwT1d298AFMGa7jqQkGiZJJ97uQKPuMImm19aBQPZcPHh2x
|
||||
qFeb1+INmWd/AoIMnI7dEwMok7XLfrV4BhC/e1XD9xCLIEoc+fGy+vHHRJ0Kzu+N
|
||||
+ej/0cFp7I5fEczJmNUcM+Jbek003HbDzdtMk9EIeG88mu50OR7NZR7JNcw7K57X
|
||||
SRCOWIWwEFuQHvFe1ZIEk/kzxp13Y9EzRluY/5qo9d/3hCHiiCh6pMYNnyV+DXNb
|
||||
1VNKkHmUNxTzyHV21BwyUb9YFnTVjRi2U/Sry5GojKPKPFw1tl9iVzdadSi3TSaq
|
||||
6lDapBxVAgMBAAECggEADJFN6K9OWhYX1PcEWUgOLxqdCLd95Iccsfxot7ekcMUP
|
||||
A4WnHRyACLqor9c2V3o2//IpU2fnB2IXu6ISmRd3WKl3hm4vnZmoIJeTpQm9Iv/g
|
||||
+fqkyrbIgktcHDJUySal+n+jiYFNW2B1h1xXUT/scMz+FthNJg1Azfi0vorMFjCk
|
||||
SBOSo7BQ2hiQ83FneVJU1TsxD4S4IBLx9fF6AW05norRmvm17Ip2pKk4QYzKiOI3
|
||||
NIoSwbOgU0Vp1X3MMlilM5ZZdN3a9lI6lfBZE682WOxBmH67mKMQnR8aC+nwynQ+
|
||||
45pUQIn8Fjx2hQHehbD7ZNw9Nob9AyGWqWGKFV74IQKBgQDox/j7dCHNm1mz1QRm
|
||||
Q2YyN4OGXJI97t9Gd8UdNCv5bAkdx2cR2lmP3tRc6iAzrfNIpPCGaXo1vwJx477X
|
||||
wO95W2b4hfm3j6v0cqRbsFzzVIHZUB77pXfAJfZeICpoqu0vxn5nb+yPgzgmToLX
|
||||
pbIDdqWafzzrDLTmLCfwfKDI9QKBgQDg3tgpAXo8WCL4phNuW44XQ172lPTijpn+
|
||||
wj1Z0rBrS806gL/+QvZZLS1WCym/QBV7TgGlxIJbmAfghcGyin3NliskSHAiccxG
|
||||
/9eCSQes8czfsVj6qmBMwyff5r+wmk662qV0u07UHmuykYk3Dgs/zYdwq2SsTlL4
|
||||
Y9eRjutp4QKBgQCONg0wYcR8/hmROeRULXzz1OJvZYKaf6K8RFOSAduTp6LyJG4d
|
||||
hA4PTQzkLsy5hd4JVWr0UuAskaMGvSJMYTxsIaEI16C1ufpNfvRWZ6qBpfEmOEKV
|
||||
boN4Sjj3TCNcioAZHeT/gGs/SeU10eUxpbLZVtTZTD6FQuAJdpR34UvBOQKBgFNM
|
||||
mXxPLM2vxHyhYK9PwQoDDel/8lr+gjMqFvnwHyQP911FllmEyqbsIlAuYG+VOJ/t
|
||||
nJSgf72YSsq0IbWWsdV3XFHbd5Z62zYtzdJYZTx+cesnUhPBC11EKcA6RSYRczqq
|
||||
hgIA5MmU30ZNvSukyyv+Yb6t7uQZO4kByzgDXldhAoGBALgRkAHxgbKUXp5XyCDJ
|
||||
e8dwVx0g9tfDM/DEZtU/Si5oUaunBaPV/Byov7OXOT02V8JLnA5ChUYgwUFI030x
|
||||
QL/3eK12Qh5Gb9VabvYCicDRk4GzmqZU9Wcvm1zgbUr5jY8Lou44nFjol/Y1m70n
|
||||
51WZbVkkWmBZO5m3NqN66SkJ
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,115 +0,0 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number:
|
||||
28:96:2e:a1:40:e0:7e:f1:fb:63:1a:f4:53:6f:ce:fb
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: CN=Easy-RSA CA
|
||||
Validity
|
||||
Not Before: Apr 29 12:09:35 2026 GMT
|
||||
Not After : Aug 1 12:09:35 2028 GMT
|
||||
Subject: CN=example_ssl_rcps_rcpm_cert
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (2048 bit)
|
||||
Modulus:
|
||||
00:ae:11:46:ef:d1:81:34:dd:23:5d:54:40:f3:9c:
|
||||
85:35:95:a6:91:57:92:5c:bf:eb:40:34:69:eb:db:
|
||||
c0:86:3c:7b:ff:9c:d7:ba:0e:41:57:84:15:cd:94:
|
||||
f1:48:63:50:9c:34:97:ee:be:be:b0:27:d8:fd:cd:
|
||||
8a:cf:85:ff:08:1f:07:d8:28:96:0e:e4:2d:d0:8b:
|
||||
df:a8:fa:41:47:a0:a2:80:2e:2e:58:01:cc:6f:43:
|
||||
5c:c2:fb:84:a7:ff:9e:97:bb:b3:a3:1f:63:64:73:
|
||||
8d:73:dd:f4:7e:96:d7:6b:b3:cb:e2:35:59:55:e0:
|
||||
e7:e3:c0:41:f8:b6:0f:c5:46:4c:cd:0e:91:80:ef:
|
||||
e3:43:f0:72:26:12:10:be:83:a2:db:23:2d:b4:b1:
|
||||
07:5a:b1:b3:10:9c:09:69:98:42:79:81:77:5e:22:
|
||||
e4:71:47:70:27:15:2c:a7:13:c2:6d:44:59:b4:73:
|
||||
c9:bb:27:7f:d6:e8:3d:85:bb:36:f6:cb:71:36:11:
|
||||
b1:99:1a:1d:1a:15:dd:cd:65:7f:cd:cc:10:00:49:
|
||||
ed:07:2d:7b:15:88:be:73:ba:1d:15:69:bc:d3:02:
|
||||
55:ea:dc:2c:3f:0b:cd:18:57:59:7a:e3:09:b2:89:
|
||||
cd:d6:e7:f6:95:c4:2e:8a:53:2b:a8:96:82:94:53:
|
||||
00:77
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
X509v3 Subject Key Identifier:
|
||||
60:BD:48:06:68:15:D4:DC:ED:EE:E4:C7:B1:9F:C4:93:6D:50:3A:77
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB
|
||||
DirName:/CN=Easy-RSA CA
|
||||
serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73
|
||||
X509v3 Extended Key Usage:
|
||||
TLS Web Server Authentication
|
||||
X509v3 Key Usage:
|
||||
Digital Signature, Key Encipherment
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:127.0.0.1, IP Address:127.0.0.1
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Signature Value:
|
||||
c5:35:61:58:23:e2:69:da:6c:d5:41:ab:a8:70:f4:dd:cc:a0:
|
||||
a3:3d:84:89:93:b6:7f:69:7d:10:35:9d:c5:d1:0d:db:d2:d7:
|
||||
36:af:d4:54:30:14:a7:5d:31:ca:5c:13:92:d5:60:50:f8:56:
|
||||
4a:cb:16:b1:b3:b1:03:bf:96:53:77:1f:4a:0f:9c:29:2b:bf:
|
||||
a4:e0:da:6f:ad:13:c7:2d:8e:18:c4:72:50:17:ed:1f:36:51:
|
||||
7a:12:9f:fc:a6:d6:c8:55:e0:db:ea:16:d6:22:0d:a2:cb:eb:
|
||||
b2:ba:07:92:2f:db:33:d6:a2:0c:ec:89:29:f1:96:40:e5:0b:
|
||||
e6:1f:08:50:d6:29:87:a8:20:b2:e2:17:50:25:ff:53:36:ee:
|
||||
7f:ce:e6:1d:ed:b3:16:61:18:42:a9:17:9e:a6:86:0d:a5:fc:
|
||||
f9:42:c8:50:48:74:72:35:eb:8c:ff:4d:e8:98:88:a0:b4:b3:
|
||||
d0:82:b3:2f:ea:19:d7:d5:ac:47:35:96:24:37:34:0c:7a:a2:
|
||||
e0:4d:99:a7:55:61:85:1e:7e:6a:23:77:f5:07:13:e6:50:5c:
|
||||
65:00:13:f6:b5:4b:5b:8c:11:c3:5d:af:ba:41:e9:84:1d:f1:
|
||||
a4:70:16:28:c2:be:6e:d8:67:38:c5:a0:ba:8a:64:6f:27:ce:
|
||||
63:a0:92:9b
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDhDCCAmygAwIBAgIQKJYuoUDgfvH7Yxr0U2/O+zANBgkqhkiG9w0BAQsFADAW
|
||||
MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yNjA0MjkxMjA5MzVaFw0yODA4MDEx
|
||||
MjA5MzVaMCUxIzAhBgNVBAMMGmV4YW1wbGVfc3NsX3JjcHNfcmNwbV9jZXJ0MIIB
|
||||
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhFG79GBNN0jXVRA85yFNZWm
|
||||
kVeSXL/rQDRp69vAhjx7/5zXug5BV4QVzZTxSGNQnDSX7r6+sCfY/c2Kz4X/CB8H
|
||||
2CiWDuQt0IvfqPpBR6CigC4uWAHMb0NcwvuEp/+el7uzox9jZHONc930fpbXa7PL
|
||||
4jVZVeDn48BB+LYPxUZMzQ6RgO/jQ/ByJhIQvoOi2yMttLEHWrGzEJwJaZhCeYF3
|
||||
XiLkcUdwJxUspxPCbURZtHPJuyd/1ug9hbs29stxNhGxmRodGhXdzWV/zcwQAEnt
|
||||
By17FYi+c7odFWm80wJV6twsPwvNGFdZeuMJsonN1uf2lcQuilMrqJaClFMAdwID
|
||||
AQABo4G+MIG7MAkGA1UdEwQCMAAwHQYDVR0OBBYEFGC9SAZoFdTc7e7kx7GfxJNt
|
||||
UDp3MFEGA1UdIwRKMEiAFLrNjgN3SkkWu9YubKOfS8NMWpWroRqkGDAWMRQwEgYD
|
||||
VQQDDAtFYXN5LVJTQSBDQYIUEv1f0yjVtkr+RNYLItZ33eTJwHMwEwYDVR0lBAww
|
||||
CgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMBoGA1UdEQQTMBGCCTEyNy4wLjAuMYcE
|
||||
fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAxTVhWCPiadps1UGrqHD03cygoz2EiZO2
|
||||
f2l9EDWdxdEN29LXNq/UVDAUp10xylwTktVgUPhWSssWsbOxA7+WU3cfSg+cKSu/
|
||||
pODab60Txy2OGMRyUBftHzZRehKf/KbWyFXg2+oW1iINosvrsroHki/bM9aiDOyJ
|
||||
KfGWQOUL5h8IUNYph6ggsuIXUCX/Uzbuf87mHe2zFmEYQqkXnqaGDaX8+ULIUEh0
|
||||
cjXrjP9N6JiIoLSz0IKzL+oZ19WsRzWWJDc0DHqi4E2Zp1VhhR5+aiN39QcT5lBc
|
||||
ZQAT9rVLW4wRw12vukHphB3xpHAWKMK+bthnOMWguopkbyfOY6CSmw==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuEUbv0YE03SNd
|
||||
VEDznIU1laaRV5Jcv+tANGnr28CGPHv/nNe6DkFXhBXNlPFIY1CcNJfuvr6wJ9j9
|
||||
zYrPhf8IHwfYKJYO5C3Qi9+o+kFHoKKALi5YAcxvQ1zC+4Sn/56Xu7OjH2Nkc41z
|
||||
3fR+ltdrs8viNVlV4OfjwEH4tg/FRkzNDpGA7+ND8HImEhC+g6LbIy20sQdasbMQ
|
||||
nAlpmEJ5gXdeIuRxR3AnFSynE8JtRFm0c8m7J3/W6D2Fuzb2y3E2EbGZGh0aFd3N
|
||||
ZX/NzBAASe0HLXsViL5zuh0VabzTAlXq3Cw/C80YV1l64wmyic3W5/aVxC6KUyuo
|
||||
loKUUwB3AgMBAAECggEABGfLiSZJmYeUmDgZrLtkDlx16sJx9zGkR+u2V+cn6D3U
|
||||
+uiCoo2EedfjSrYKT/AI35Xf19sGrc6ptJgPJfwY3aEWFxJv5HtB7ZVHWS98QiPT
|
||||
+QqHgb1fPzGlQgoQ7Bo8GVBW1joPz0Bdbsv0ntTn1CyowauNUe8Z71mzpxJ0iQ7u
|
||||
Z8LNoD7INEAZDBjHVov6pLGDHS9KFxGy20WG549mE37I4QxyxetJEgmuyhJkZOQ4
|
||||
noEWiCMQjGsSg4YuSc1GS1jAVf3p2g3/TiheD/31r1jleY/T5s2qYC6MJ4vY+7yA
|
||||
5sl7m8A47i0lHSKBdR3nWz+vXEsxB0nXK3Sulyt9AQKBgQDYRZ1nEe32CCZcWn/7
|
||||
nG+9e6XNOe9E25skuvY/uEpikt92kdnyZOgHFfwM9Nv+w2IWGLZ8MmDDJ6/4hGvj
|
||||
fJjcOUb3d/SJiivPvdRC9GYMrDUZe5AJ3p6fPqi4IeZOw7bMTcVB6aHAr9KoO//J
|
||||
2t0WNvwzkOyl6KlhaQEIDK8bOQKBgQDOCvWvUEg8nHaKcmN0uQaKuPvCorhnyqUT
|
||||
VFqLlSrYC79ffHCwp2y8nkFUnxpeHXENrtHtcrdnKBNkgGEn2l4xs63CjhKrBuIG
|
||||
bDjrtp3vKlHRhQjX6HkrEEuUk52wYzuX7CfU8nTZnrtV74MocOEewJVW+84Rtwiw
|
||||
vIUcgfgJLwKBgAbgj9TLOSntsGqXZiJ2Iwd/exI/mWAzK4fLejEkhxkDWp/Gm4ud
|
||||
sdMn28/9qVE8nU3ek073uyP5ixr3+wZM2/+EwsDzy47kGeiNPMa0Rtp4T2f0CeyG
|
||||
a7zcnTjduxkeGB3/CxrBdydNcAFxhvzAPO+L6BErtpq//0Ldt+6tmJPhAoGAFxEh
|
||||
Cjx5qdd2ae9+dO3V7qfg/5xJ+sy0CGL0NBZCEqfWB/GdiBlmUgOBmuCpCgpPwtFk
|
||||
jSm/oJva9/BrcBPBYd0Uweg37M+7dC6ffLwYGFNrj4JOSCWtkwWjAII6MCob3NlC
|
||||
aFOwg0CDBo7m5xskCNZUocVU/6S3I1onqNZgF18CgYAczBf1NS4VPZvebrutTBEH
|
||||
zyUX3XU9mR+dy0ncCGNVS8zYMtz7cweZInzNB2cTOfisIHzzdOnazm7D9uzPsREi
|
||||
pKgaL+ErWYDlGiDTxMtGSRPTWGocYBYdU6y/0bobhZb0qyvyRhpGvPK0ReMUuvqu
|
||||
FkNgoQ1lo0n6vawvxWW8Mw==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
EASYRSA=/usr/share/easy-rsa/easyrsa
|
||||
CA_NAME="example_ssl_rcp_ca_cert"
|
||||
|
||||
export EASYRSA_PASSIN=pass:test
|
||||
export EASYRSA_PASSOUT=pass:test
|
||||
|
||||
echo "Cleaning up..."
|
||||
rm -rf ./ca
|
||||
rm -rf ./*.pem
|
||||
rm -rf ./*.key
|
||||
rm -rf ./*.crt
|
||||
|
||||
echo "Creating CA cert..."
|
||||
mkdir -p ./ca
|
||||
cd ./ca
|
||||
$EASYRSA init-pki
|
||||
cp ../vars ./pki/
|
||||
$EASYRSA --batch build-ca
|
||||
cp ./pki/ca.crt ../$CA_NAME.crt
|
||||
|
||||
echo "Creating server certs..."
|
||||
# Secures connection between RCP-Client and RCP-Server:
|
||||
$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcpc_rcps_cert nopass
|
||||
|
||||
# Secures connection between RCP-Module and RCP-Server (module description):
|
||||
$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcpm_rcps_cert nopass
|
||||
|
||||
# Secures connection between RCP-Server and RCP-Module (command execution):
|
||||
$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcps_rcpm_cert nopass
|
||||
|
||||
echo "Collecting server certs..."
|
||||
cp ./pki/issued/* ../
|
||||
cp ./pki/private/* ../
|
||||
cd ..
|
||||
rm ./ca.key
|
||||
|
||||
echo "Merging server certs..."
|
||||
for CRT in ./*.crt; do
|
||||
CRT_NAME=`basename ${CRT%.*}`
|
||||
if [ -f $CRT_NAME.key ]; then
|
||||
cat $CRT_NAME.crt $CRT_NAME.key > $CRT_NAME.pem
|
||||
rm $CRT_NAME.key
|
||||
rm $CRT_NAME.crt
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Finalizing..."
|
||||
rm -rf ./ca
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
. ./params.cfg
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/csv-encrypt-columns.py \
|
||||
--csv-column-key kic:$CSV_COLUMN_KEY \
|
||||
--csv-column-key kid:$CSV_COLUMN_KEY \
|
||||
--csv-column-key kik:$CSV_COLUMN_KEY \
|
||||
card_data.csv
|
||||
@@ -1,36 +0,0 @@
|
||||
# Verbosity switch passed to all components (comment-out to disable verbose mode)
|
||||
#VERBOSE="--verbose"
|
||||
|
||||
# PYSIM_DIR passed to all components
|
||||
PYSIM_DIR=../../../ # Points to the psyim top directory
|
||||
|
||||
# CSV column key to decrypt KIC, KID and KIK in csv_data.csv.encr
|
||||
# (use encrypt_card_data.sh to regenerate csv_data.csv.encr from csv_data.csv)
|
||||
CSV_COLUMN_KEY="00112233445566778899AABBCCDDEEFF"
|
||||
|
||||
# PCSC reader that the RCP Client shall use
|
||||
PCSC_READER=0
|
||||
|
||||
# CA of the certificates used in this example
|
||||
CERT_DIR="./certs"
|
||||
CA_CERT="$CERT_DIR/example_ssl_rcp_ca_cert.crt"
|
||||
|
||||
# Network interface where RCP Clients connect
|
||||
RCPC_SERVER_PORT=8000
|
||||
RCPC_SERVER_ADDR="127.0.0.1"
|
||||
RCPC_SERVER_CERT="$CERT_DIR/example_ssl_rcpc_rcps_cert.pem"
|
||||
RCPC_SERVER_URI="wss://$RCPC_SERVER_ADDR:$RCPC_SERVER_PORT"
|
||||
|
||||
# Network interface where RCP Modules connect
|
||||
RCPM_SERVER_PORT=8010
|
||||
RCPM_SERVER_ADDR="127.0.0.1"
|
||||
RCPM_SERVER_CERT="$CERT_DIR/example_ssl_rcpm_rcps_cert.pem"
|
||||
RCPM_SERVER_URI="wss://$RCPM_SERVER_ADDR:$RCPM_SERVER_PORT"
|
||||
|
||||
# Network interface where the (example) RCP Module binds its Command Server to.
|
||||
# The command server is used by the RCP Server to run the command requested
|
||||
# by the user. Each module needs a dedicated port. The address and port is
|
||||
# automatically forwarded to the RCP Server.
|
||||
RCPM_CMD_SERVER_PORT=8020
|
||||
RCPM_CMD_SERVER_ADDR="127.0.0.1"
|
||||
RCPM_CMD_SERVER_CERT="$CERT_DIR/example_ssl_rcps_rcpm_cert.pem"
|
||||
@@ -1,130 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Philipp Maier
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU 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 pathlib import Path
|
||||
from pySim.log import PySimLogger
|
||||
from argparse import Namespace
|
||||
from pySim.global_platform import GpCardKeyset, SCP02, ADF_SD
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from osmocom.utils import h2b, b2h
|
||||
from rcp_module_utils import rcpm_setup_argparse, rcpm_run_module, RcpModule, RcpModuleHdlr
|
||||
|
||||
log = PySimLogger.get(Path(__file__).stem)
|
||||
option_parser = rcpm_setup_argparse("Example Module")
|
||||
|
||||
class ExmpleModule(RcpModule):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
log.info("rcpm_run_module was called with the following additional arguments:")
|
||||
log.info("%s, %s", str(args), str(kwargs))
|
||||
|
||||
name = 'rcp_module'
|
||||
cmd_descr = [{'name' : 'reset',
|
||||
'help': 'reset the card',
|
||||
'args' : []},
|
||||
{'name' : 'read_binary',
|
||||
'help': 'read binary data from a transparent file.',
|
||||
'args' : [{ 'name' : '--fid',
|
||||
'spec' : {'required' : True,
|
||||
'help' : 'File identifier to of the file to read',
|
||||
'action' : 'append',
|
||||
'pytype' : 'str'},
|
||||
}
|
||||
]},
|
||||
{'name' : 'read_record',
|
||||
'help': 'read binary data from a transparent file.',
|
||||
'args' : [{ 'name' : '--fid',
|
||||
'spec' : {'required' : True,
|
||||
'help' : 'File identifier to of the file to read',
|
||||
'action' : 'append',
|
||||
'pytype' : 'str'},
|
||||
},
|
||||
{ 'name' : '--record',
|
||||
'spec' : {'required' : True,
|
||||
'help' : 'File record to read',
|
||||
'default' : 1,
|
||||
'pytype' : 'int'},
|
||||
}
|
||||
]},
|
||||
{'name' : 'unlock_aram',
|
||||
'help': 'unlock a locked ARA-M applet on a sysmoISIM-SJA5',
|
||||
'args' : [],
|
||||
'get_keys' : {'uicc' : ['KIC', 'KID', 'KIK']}}
|
||||
]
|
||||
suitable_for = [{'atr' : '3b9f96801f878031e073fe211b674a357530350265f8'}]
|
||||
|
||||
def cmd_reset(self, hdlr: RcpModuleHdlr) -> int:
|
||||
hdlr.print("resetting UICC/eUICC ...")
|
||||
hdlr.card._scc.reset_card()
|
||||
hdlr.print("ATR is: %s" % hdlr.card._scc.get_atr())
|
||||
return 0
|
||||
|
||||
def cmd_read_binary(self, hdlr: RcpModuleHdlr) -> int:
|
||||
fid = hdlr.cmd_args.fid
|
||||
hdlr.print("reading transparent file: %s ..." % fid)
|
||||
(res, _) = hdlr.card._scc.read_binary(fid)
|
||||
hdlr.print("file content is: %s" % res)
|
||||
return 0
|
||||
|
||||
def cmd_read_record(self, hdlr: RcpModuleHdlr) -> int:
|
||||
fid = hdlr.cmd_args.fid
|
||||
record = hdlr.cmd_args.record
|
||||
hdlr.print("reading linear-fixed file: %s ..." % fid)
|
||||
(res, _) = hdlr.card._scc.read_record(fid, record)
|
||||
hdlr.print("file content is: %s" % res)
|
||||
return 0
|
||||
|
||||
def cmd_unlock_aram(self, hdlr: RcpModuleHdlr) -> int:
|
||||
# Select ADF.ISD
|
||||
hdlr.print("Selecting ADF.ISD ...")
|
||||
hdlr.lchan.scc.send_apdu_checksw("00a4040408a00000000300000000")
|
||||
|
||||
# Establish secure channel
|
||||
hdlr.print("Establishing secure channel ...")
|
||||
key_ver = 112
|
||||
key_enc = hdlr.keys_uicc['KIC']
|
||||
key_mac = hdlr.keys_uicc['KID']
|
||||
key_dek = hdlr.keys_uicc['KIK']
|
||||
security_level = 3
|
||||
host_challenge_len = 8
|
||||
host_challenge = get_random_bytes(host_challenge_len)
|
||||
kset = GpCardKeyset(key_ver, h2b(key_enc), h2b(key_mac), h2b(key_dek))
|
||||
scp = SCP02(card_keys=kset)
|
||||
ADF_SD.establish_scp(hdlr.lchan.scc, scp, host_challenge, security_level)
|
||||
|
||||
# To prove that it works, we need to do something that actually requires to be authenticated
|
||||
# via a secure channel. In this example we will send an unlock command to the ARA-M applet
|
||||
# found on any sysmoISIM-SJA5 card. (see also: https://gitea.osmocom.org/sim-card/aram-applet)
|
||||
hdlr.print("Unlocking ARA-M applet ...")
|
||||
ara_m_aid = "a00000015141434c00"
|
||||
ADF_SD.install(hdlr.lchan.scc, 0x20, 0x00, "0000%02x%s000000" % (len(ara_m_aid) // 2, ara_m_aid))
|
||||
ADF_SD.store_data(hdlr.lchan.scc, h2b("A2"), structure = 'ber_tlv')
|
||||
|
||||
# Release the secure channel
|
||||
hdlr.print("Done, releasing secure channel ...")
|
||||
ADF_SD.release_scp(hdlr.lchan.scc)
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = option_parser.parse_args()
|
||||
rcpm_run_module(opts, ExmpleModule,
|
||||
"arg1", "arg2", "arg3",
|
||||
kwarg1="kwarg1", kwarg2="kwarg2", kwarg3="kwarg3")
|
||||
@@ -1,16 +0,0 @@
|
||||
How to try:
|
||||
|
||||
Go to the directory that contains the usage example:
|
||||
cd pysim/contrib/rcp/usage_example
|
||||
|
||||
Edit card_data.csv to fill in the SCP02 keys for the ISD of your sysmoISIM-SJA5
|
||||
|
||||
Start the RCP Server:
|
||||
./start_rcp_server.sh
|
||||
|
||||
Start the RCP Module:
|
||||
./start_rcp_module.sh
|
||||
|
||||
Run the exmple scripts:
|
||||
./run_rcp_client.sh
|
||||
(it is also possible to call the run_rcp_client_*.sh scripts individually)
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "basic help"
|
||||
echo "===================================================================================="
|
||||
./run_rcp_client_help.sh
|
||||
echo "===================================================================================="
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
echo "help for which commands are available"
|
||||
echo "===================================================================================="
|
||||
./run_rcp_client_help_cmd.sh
|
||||
echo "===================================================================================="
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
echo "help for specific commands"
|
||||
echo "===================================================================================="
|
||||
./run_rcp_client_help_cmd_specific.sh
|
||||
echo "===================================================================================="
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
echo "run specific RCP commands"
|
||||
echo "===================================================================================="
|
||||
./run_rcp_client_cmd.sh
|
||||
echo "===================================================================================="
|
||||
echo ""
|
||||
echo ""
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
. ./params.cfg
|
||||
|
||||
set -x
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI\
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
rcp_module_reset
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI \
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
rcp_module_read_binary --fid 3f00 --fid 2fe2
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI \
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
rcp_module_read_record --fid 3f00 --fid 2f00 --record 1
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI \
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
rcp_module_unlock_aram
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
. ./params.cfg
|
||||
|
||||
set -x
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
-h
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
. ./params.cfg
|
||||
|
||||
set -x
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI \
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
-h
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
. ./params.cfg
|
||||
|
||||
set -x
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI \
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
rcp_module_reset --help
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI \
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
rcp_module_read_binary --help
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI \
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
rcp_module_read_record --help
|
||||
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||
--uri $RCPC_SERVER_URI \
|
||||
--ca-cert $CA_CERT \
|
||||
-p $PCSC_READER \
|
||||
rcp_module_unlock_aram --help
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
. ./params.cfg
|
||||
|
||||
set -x
|
||||
PYTHONPATH=$PYSIM_DIR:$PYSIM_DIR/contrib/rcp ./rcp_module.py $VERBOSE \
|
||||
--uri $RCPM_SERVER_URI \
|
||||
--rcps-ca-cert $CA_CERT \
|
||||
--rcpm-cmd-server-addr $RCPM_CMD_SERVER_ADDR \
|
||||
--rcpm-cmd-server-port $RCPM_CMD_SERVER_PORT \
|
||||
--rcpm-cmd-server-cert $RCPM_CMD_SERVER_CERT \
|
||||
--column-key kic:$CSV_COLUMN_KEY \
|
||||
--column-key kid:$CSV_COLUMN_KEY \
|
||||
--column-key kik:$CSV_COLUMN_KEY
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
. ./params.cfg
|
||||
|
||||
set -x
|
||||
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_server.py $VERBOSE \
|
||||
--rcpc-server-addr $RCPC_SERVER_ADDR \
|
||||
--rcpc-server-port $RCPC_SERVER_PORT \
|
||||
--rcpc-server-cert $RCPC_SERVER_CERT \
|
||||
--rcpm-server-addr $RCPM_SERVER_ADDR \
|
||||
--rcpm-server-port $RCPM_SERVER_PORT \
|
||||
--rcpm-server-cert $RCPM_SERVER_CERT \
|
||||
--rcpm-module-ca-cert $CA_CERT \
|
||||
--csv ./card_data.csv.encr
|
||||
@@ -1,524 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""TLS-PSK HTTP Remote Administration Server for SCP81 / RAM over HTTP"""
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Eric Wild <ewild@sysmocom.de>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# The card (specifically a SD supporting SCP81) is the TLS client:
|
||||
# - it opens a TCP connection to this server,
|
||||
# - performs a TLS handshake authenticated with a PSK,
|
||||
# - and then drives the HTTP admin loop of to fetch remote APDU command strings
|
||||
# - and posts back their responses.
|
||||
# This program is the server side of that exchange, it:
|
||||
# - accepts the PSK-TLS connection,
|
||||
# - hands the card a queue of commands
|
||||
# - and logs the decoded responses.
|
||||
#
|
||||
# The two TS 102 226 annex B figure B.1 administration modes are supported over the
|
||||
# same session, selected with --mode:
|
||||
# ram GP Amendment B RAM:
|
||||
# command is handled by (--targeted-application) a SD
|
||||
# rfm ETSI TS 102 226 RFM/RAM:
|
||||
# command is routed to the Receiving/RFM Application specified by
|
||||
# --targeted-application, for example UICC-filesystem/USIM-ADF RFM app.
|
||||
#
|
||||
# Remote APDU command/response bodies use the Expanded Remote Application
|
||||
# data format.
|
||||
#
|
||||
|
||||
import ssl
|
||||
import socket
|
||||
import logging
|
||||
import argparse
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Callable, Dict, Tuple
|
||||
|
||||
from osmocom.utils import h2b, b2h
|
||||
|
||||
from pySim.ota import encode_expanded_cmd, decode_expanded_resp
|
||||
|
||||
logger = logging.getLogger(Path(__file__).stem)
|
||||
|
||||
# Amendment B section 3.4
|
||||
ADMIN_PROTOCOL = 'globalplatform-remote-admin/1.0'
|
||||
CT_COMMAND = 'application/vnd.globalplatform.card-content-mgt;version=1.0'
|
||||
CT_RESPONSE = 'application/vnd.globalplatform.card-content-mgt-response;version=1.0'
|
||||
|
||||
# TS 102 226 annex B, figure B.1 RFM/RAM over HTTPS content types
|
||||
CT_RFM_COMMAND = 'application/vnd.etsi.scp.command-data;version=1.0'
|
||||
CT_RFM_RESPONSE = 'application/vnd.etsi.scp.response-data;version=1.0'
|
||||
|
||||
MODE_CONTENT_TYPE = {
|
||||
'ram': CT_COMMAND, # GP Amendment B RAM: target = a Security Domain
|
||||
'rfm': CT_RFM_COMMAND, # ETSI TS 102 226 RFM/RAM: target = an application
|
||||
}
|
||||
|
||||
# Amendment B Table 3-2. The 3DES and NULL suites are left out and can be enabled with
|
||||
# --ciphers / --seclevel.
|
||||
DEFAULT_CIPHERS = ':'.join([
|
||||
'PSK-AES128-CBC-SHA256', # TLS_PSK_WITH_AES_128_CBC_SHA256, TLS 1.2
|
||||
'PSK-AES128-CBC-SHA', # TLS_PSK_WITH_AES_128_CBC_SHA, TLS 1.0/1.1
|
||||
])
|
||||
|
||||
TLS_VERSION_MAP = {
|
||||
'1.0': ssl.TLSVersion.TLSv1,
|
||||
'1.1': ssl.TLSVersion.TLSv1_1,
|
||||
'1.2': ssl.TLSVersion.TLSv1_2,
|
||||
'1.3': ssl.TLSVersion.TLSv1_3,
|
||||
}
|
||||
|
||||
|
||||
def format_aid(aid: str) -> str:
|
||||
"""AID -> //aid/<RID>/<PIX> for X-Admin-Targeted-Application from Amendment B section 3.4.2
|
||||
First 5 bytes RID, the PIX the remainder, string in //aid/ notation is passed through."""
|
||||
if aid.startswith('//aid/'):
|
||||
return aid
|
||||
aid = aid.replace(' ', '').lower()
|
||||
if len(aid) < 10:
|
||||
raise ValueError('AID %r is shorter than the 5 byte RID' % aid)
|
||||
rid, pix = aid[:10], aid[10:]
|
||||
return '//aid/%s/%s' % (rid, pix)
|
||||
|
||||
|
||||
def make_ssl_context(psk: bytes, identity: str, *,
|
||||
ciphers: str = DEFAULT_CIPHERS,
|
||||
min_tls: str = '1.2', max_tls: str = '1.3',
|
||||
seclevel: Optional[int] = None,
|
||||
identity_hint: Optional[str] = None,
|
||||
allow_any_identity: bool = False,
|
||||
extra_psks: Optional[Dict[str, bytes]] = None) -> ssl.SSLContext:
|
||||
"""PSK SSLContext, resolvesg the key from the client psk_identity
|
||||
|
||||
extra_psks can carry additional identity->key mappings.
|
||||
allow_any_identity can be used for debugging
|
||||
"""
|
||||
# the PSK callback must return immutable bytes, h2b() gives a bytearray
|
||||
psk = bytes(psk)
|
||||
keymap: Dict[str, bytes] = {identity: psk}
|
||||
if extra_psks:
|
||||
keymap.update({k: bytes(v) for k, v in extra_psks.items()})
|
||||
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ctx.minimum_version = TLS_VERSION_MAP[min_tls]
|
||||
ctx.maximum_version = TLS_VERSION_MAP[max_tls]
|
||||
cipher_str = ciphers
|
||||
if seclevel is not None:
|
||||
# @SECLEVEL=0 is to enable NULL/3DES/legacy PSK suites
|
||||
cipher_str = '%s:@SECLEVEL=%d' % (ciphers, seclevel)
|
||||
if cipher_str:
|
||||
ctx.set_ciphers(cipher_str)
|
||||
|
||||
def psk_server_callback(client_identity: Optional[str]) -> bytes:
|
||||
if allow_any_identity:
|
||||
logger.info('PSK handshake: identity=%r (ACEPTING ANY!)', client_identity)
|
||||
return psk
|
||||
key = keymap.get(client_identity)
|
||||
if key is None:
|
||||
logger.warning('PSK handshake: unknown identity %r (known: %r) -> rejecting',
|
||||
client_identity, list(keymap.keys()))
|
||||
return b'' # empty PSK aborts handshake
|
||||
logger.info('PSK handshake: identity=%r resolved', client_identity)
|
||||
return key
|
||||
|
||||
ctx.set_psk_server_callback(psk_server_callback, identity_hint=identity_hint)
|
||||
return ctx
|
||||
|
||||
|
||||
class HttpRequest:
|
||||
"""A parsed HTTP request (request line + headers + body)."""
|
||||
__slots__ = ('method', 'uri', 'version', 'headers', 'body')
|
||||
|
||||
def __init__(self, method: str, uri: str, version: str,
|
||||
headers: Dict[str, str], body: bytes):
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
self.headers = headers # lower cased field names
|
||||
self.body = body
|
||||
|
||||
def get(self, name: str, default=None) -> Optional[str]:
|
||||
return self.headers.get(name.lower(), default)
|
||||
|
||||
|
||||
# http.client bound on a single HTTP line.
|
||||
MAX_LINE = 65536
|
||||
|
||||
|
||||
def _read_line(rfile) -> bytes:
|
||||
"""readline() with a bound. reaching the bound without a
|
||||
terminator means the card is out of sync somehow, not that the line is long."""
|
||||
line = rfile.readline(MAX_LINE)
|
||||
if line and not line.endswith(b'\n'):
|
||||
raise ValueError('HTTP line longer than %u bytes' % MAX_LINE)
|
||||
return line
|
||||
|
||||
|
||||
def _read_chunked_body(rfile) -> bytes:
|
||||
"""Read a Transfer-Encoding: chunked body. Amendment B section 3.4.1 lets
|
||||
the card send its response string with either a Content-Length or chunked"""
|
||||
out = bytearray()
|
||||
while True:
|
||||
size_line = _read_line(rfile)
|
||||
if not size_line:
|
||||
break
|
||||
size = int(size_line.split(b';', 1)[0].strip() or b'0', 16)
|
||||
if size == 0:
|
||||
# consume trailer headers up to the terminating blank line
|
||||
while _read_line(rfile) not in (b'\r\n', b'\n', b''):
|
||||
pass
|
||||
break
|
||||
chunk = rfile.read(size)
|
||||
if len(chunk) != size:
|
||||
raise ValueError('chunked body ended after %u of %u bytes' % (len(chunk), size))
|
||||
out += chunk
|
||||
_read_line(rfile) # trailing CRLF after the chunk data
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def read_http_request(rfile, send: Optional[Callable[[bytes], None]] = None) -> Optional[HttpRequest]:
|
||||
"""Read one HTTP request from a buffered binary reader or None when closed"""
|
||||
request_line = _read_line(rfile)
|
||||
if not request_line:
|
||||
return None
|
||||
parts = request_line.rstrip(b'\r\n').decode('iso-8859-1').split(' ')
|
||||
if len(parts) < 3:
|
||||
raise ValueError('Malformed HTTP request line: %r' % request_line)
|
||||
method, uri, version = parts[0], parts[1], parts[2]
|
||||
|
||||
headers: Dict[str, str] = {}
|
||||
while True:
|
||||
line = _read_line(rfile)
|
||||
if line in (b'\r\n', b'\n', b''):
|
||||
break
|
||||
name, _, value = line.rstrip(b'\r\n').decode('iso-8859-1').partition(':')
|
||||
headers[name.strip().lower()] = value.strip()
|
||||
|
||||
# Expect: 100-continue waits for the response before it sends the body,
|
||||
# and RFC 2616 8.2.3 (Amendment B references RFC 2616 as [HTTP])
|
||||
# requires the server to send it. Amendment B 3.4.1 does not mention this
|
||||
# header, tho, might be useless.
|
||||
if send and '100-continue' in headers.get('expect', '').lower():
|
||||
logger.info('-> 100 Continue ')
|
||||
send(b'HTTP/1.1 100 Continue\r\n\r\n')
|
||||
|
||||
body = b''
|
||||
te = headers.get('transfer-encoding', '').lower()
|
||||
if 'chunked' in te:
|
||||
body = _read_chunked_body(rfile)
|
||||
elif 'content-length' in headers:
|
||||
n = int(headers['content-length'])
|
||||
if n:
|
||||
body = rfile.read(n)
|
||||
return HttpRequest(method, uri, version, headers, body)
|
||||
|
||||
|
||||
def build_http_response(status_line: str, headers: List[Tuple[str, str]],
|
||||
body: bytes = b'') -> bytes:
|
||||
"""Serialise HTTP response. status_line 'HTTP/1.1 200 OK'."""
|
||||
lines = [status_line]
|
||||
lines += ['%s: %s' % (name, value) for name, value in headers]
|
||||
head = ('\r\n'.join(lines) + '\r\n\r\n').encode('iso-8859-1')
|
||||
return head + body
|
||||
|
||||
|
||||
def format_decoded_response(dec) -> str:
|
||||
"""hand over the data"""
|
||||
bits = ['%u command(s) executed' % dec.number_of_commands]
|
||||
for i, c in enumerate(dec.commands):
|
||||
data = c.response_data or '-'
|
||||
bits.append(' R-APDU[%u]: SW=%s data=%s' % (i, c.status_word, data))
|
||||
if dec.get('truncated'):
|
||||
bits.append(' TRUNCATED: an R-APDU returned SW 62F1, so the card cut the response data '
|
||||
'short and stopped executing the rest of the script ') # TS 102 226 5.2.1.1
|
||||
if dec.bad_format is not None:
|
||||
bits.append(' bad-format: %s' % dec.bad_format)
|
||||
if dec.immediate_action_response is not None:
|
||||
bits.append(' immediate-action-response: %s' % dec.immediate_action_response)
|
||||
if dec.script_chaining_response is not None:
|
||||
bits.append(' script-chaining-response: %s' % dec.script_chaining_response)
|
||||
return '\n'.join(bits)
|
||||
|
||||
|
||||
class AdminSession:
|
||||
|
||||
def __init__(self, command_bodies: List[bytes],
|
||||
next_uri: Optional[str] = None,
|
||||
targeted_application: Optional[str] = None,
|
||||
on_response: Optional[Callable[[object], None]] = None,
|
||||
content_type: str = CT_COMMAND):
|
||||
self.pending: List[bytes] = list(command_bodies)
|
||||
self.next_uri = next_uri # None -> echo the request URI
|
||||
self.targeted_application = targeted_application
|
||||
self.on_response = on_response
|
||||
self.content_type = content_type # Content-Type for the command body
|
||||
self.responses: List[object] = [] # decoded Containers, in order
|
||||
|
||||
def record_response(self, dec) -> None:
|
||||
self.responses.append(dec)
|
||||
if self.on_response:
|
||||
self.on_response(dec)
|
||||
|
||||
|
||||
def run_admin_loop(rfile, send: Callable[[bytes], None], session: AdminSession) -> AdminSession:
|
||||
"""Drive the admin loop for one connection.
|
||||
Just keep answering the card POST requests with the next queued command
|
||||
(200 OK + Expanded command body) until the queue is empty, end session with 204 No Content."""
|
||||
while True:
|
||||
req = read_http_request(rfile, send)
|
||||
if req is None:
|
||||
logger.info('connection closed by card')
|
||||
return session
|
||||
|
||||
if req.method != 'POST':
|
||||
logger.warning('unexpected method %s %s -> 405', req.method, req.uri)
|
||||
send(build_http_response('HTTP/1.1 405 Method Not Allowed',
|
||||
[('X-Admin-Protocol', ADMIN_PROTOCOL),
|
||||
('Connection', 'close')]))
|
||||
return session
|
||||
|
||||
proto = req.get('x-admin-protocol')
|
||||
if proto and proto != ADMIN_PROTOCOL:
|
||||
logger.warning('card X-Admin-Protocol=%r (expected %r)', proto, ADMIN_PROTOCOL)
|
||||
status = req.get('x-admin-script-status')
|
||||
resume = req.get('x-admin-resume')
|
||||
logger.info('POST %s from=%r status=%r resume=%r body=%uB',
|
||||
req.uri, req.get('x-admin-from'), status, resume, len(req.body))
|
||||
|
||||
# section 3.4.1:
|
||||
# - body with "X-Admin-Script-Status: ok" carries the previous command
|
||||
# response string (Expanded Remote response format);
|
||||
# - other status values carry no body ().
|
||||
if req.body:
|
||||
# Expanded Remote response:
|
||||
# - GP Amd B 'card-content-mgt-response'
|
||||
# - ETSI 'scp.response-data'
|
||||
logger.debug(' response Content-Type=%r raw body (%uB): %s',
|
||||
req.get('content-type'), len(req.body), b2h(req.body))
|
||||
if status in (None, 'ok'):
|
||||
try:
|
||||
dec = decode_expanded_resp(req.body)
|
||||
session.record_response(dec)
|
||||
logger.info('card response:\n%s', format_decoded_response(dec))
|
||||
except Exception as e:
|
||||
logger.error('failed to decode response body %s: %s', b2h(req.body), e)
|
||||
else:
|
||||
logger.warning('body present with status=%r; ignoring', status) # section 3.4.1
|
||||
elif status and status != 'ok':
|
||||
logger.info('card reported script-status=%r (no response body)', status)
|
||||
|
||||
if session.pending:
|
||||
body = session.pending.pop(0)
|
||||
next_uri = session.next_uri or req.uri
|
||||
headers = [('X-Admin-Protocol', ADMIN_PROTOCOL),
|
||||
('X-Admin-Next-URI', next_uri),
|
||||
('Content-Type', session.content_type)]
|
||||
if session.targeted_application:
|
||||
headers.append(('X-Admin-Targeted-Application', session.targeted_application))
|
||||
headers.append(('Content-Length', str(len(body))))
|
||||
logger.info('-> 200 OK, next command (%uB): %s', len(body), b2h(body))
|
||||
send(build_http_response('HTTP/1.1 200 OK', headers, body))
|
||||
else:
|
||||
# section 3.4.2: No more commands, end session
|
||||
# No Content-Type or body for 204
|
||||
logger.info('-> 204 No Content, ending administration session')
|
||||
send(build_http_response('HTTP/1.1 204 No Content',
|
||||
[('X-Admin-Protocol', ADMIN_PROTOCOL),
|
||||
('Connection', 'close')]))
|
||||
return session
|
||||
|
||||
|
||||
class Scp81AdminServer:
|
||||
"""Threaded TLS-PSK server that runs the Amendment B admin loop against each
|
||||
connecting card."""
|
||||
|
||||
def __init__(self, host: str, port: int, ssl_ctx: ssl.SSLContext,
|
||||
command_bodies: List[bytes],
|
||||
next_uri: Optional[str] = None,
|
||||
targeted_application: Optional[str] = None,
|
||||
on_response: Optional[Callable[[object], None]] = None,
|
||||
on_session_end: Optional[Callable[[AdminSession], None]] = None,
|
||||
content_type: str = CT_COMMAND):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ssl_ctx = ssl_ctx
|
||||
self.command_bodies = command_bodies
|
||||
self.next_uri = next_uri
|
||||
self.targeted_application = targeted_application
|
||||
self.content_type = content_type
|
||||
self.on_response = on_response
|
||||
self.on_session_end = on_session_end
|
||||
self._sock: Optional[socket.socket] = None
|
||||
self._stop = threading.Event()
|
||||
|
||||
def bind(self) -> int:
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._sock.bind((self.host, self.port))
|
||||
self._sock.listen(5)
|
||||
self._sock.settimeout(0.5)
|
||||
self.port = self._sock.getsockname()[1]
|
||||
return self.port
|
||||
|
||||
def serve_forever(self) -> None:
|
||||
if self._sock is None:
|
||||
self.bind()
|
||||
logger.info('SCP81 admin server listening on %s:%u (%u command(s) queued)',
|
||||
self.host, self.port, len(self.command_bodies))
|
||||
while not self._stop.is_set():
|
||||
try:
|
||||
conn, addr = self._sock.accept()
|
||||
except socket.timeout:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
threading.Thread(target=self._handle, args=(conn, addr), daemon=True).start()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self._stop.set()
|
||||
if self._sock is not None:
|
||||
self._sock.close()
|
||||
|
||||
def _handle(self, conn: socket.socket, addr) -> None:
|
||||
try:
|
||||
tls = self.ssl_ctx.wrap_socket(conn, server_side=True)
|
||||
except (ssl.SSLError, OSError) as e:
|
||||
logger.warning('TLS-PSK handshake with %s failed: %s', addr, e)
|
||||
try:
|
||||
conn.close()
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
logger.info('TLS-PSK established with %s: %s / %s', addr, tls.version(), tls.cipher())
|
||||
session = AdminSession(self.command_bodies, next_uri=self.next_uri,
|
||||
targeted_application=self.targeted_application,
|
||||
on_response=self.on_response,
|
||||
content_type=self.content_type)
|
||||
try:
|
||||
rfile = tls.makefile('rb')
|
||||
run_admin_loop(rfile, tls.sendall, session)
|
||||
except (ssl.SSLError, OSError, ValueError) as e:
|
||||
logger.warning('session with %s aborted: %s', addr, e)
|
||||
finally:
|
||||
try:
|
||||
tls.close()
|
||||
except OSError:
|
||||
pass
|
||||
logger.info('session with %s ended: %u response(s) collected', addr, len(session.responses))
|
||||
if self.on_session_end:
|
||||
self.on_session_end(session)
|
||||
|
||||
|
||||
def build_command_bodies(apdus: List[bytes], batch: bool = False,
|
||||
length_coding: str = 'definite') -> List[bytes]:
|
||||
"""wrpa apdus
|
||||
each C-APDU -> one cmd message + one HTTP response per APDU
|
||||
batch=True -> all C-APDUs in one Command Scripting template.
|
||||
length_coding selects the definite or indefinite Command Scripting template."""
|
||||
if not apdus:
|
||||
return []
|
||||
if batch:
|
||||
return [encode_expanded_cmd(apdus, length_coding=length_coding)]
|
||||
return [encode_expanded_cmd(a, length_coding=length_coding) for a in apdus]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='TLS-PSK HTTP Remote Administration Server for SCP81 / RAM over HTTP')
|
||||
parser.add_argument('--host', default='0.0.0.0', help='Host/IP to bind to (default: 0.0.0.0)')
|
||||
parser.add_argument('--port', type=int, default=8443, help='TCP port to bind to (default: 8443)')
|
||||
parser.add_argument('--psk', required=True,
|
||||
help='PSK TLS key, Amendment B key type 85 as hex')
|
||||
parser.add_argument('--psk-identity', required=True,
|
||||
help='Expected PSK identity string presented by the card')
|
||||
parser.add_argument('--psk-identity-hint', default=None,
|
||||
help='Optional PSK identity hint to send to the card (default: none)')
|
||||
parser.add_argument('--allow-any-identity', action='store_true',
|
||||
help='DEBUG: Accept any psk_identity')
|
||||
parser.add_argument('--ciphers', default=DEFAULT_CIPHERS,
|
||||
help='OpenSSL cipher string for TLS<=1.2')
|
||||
parser.add_argument('--min-tls', default='1.2', choices=sorted(TLS_VERSION_MAP),
|
||||
help='Minimum TLS version (default: 1.2)')
|
||||
parser.add_argument('--max-tls', default='1.3', choices=sorted(TLS_VERSION_MAP),
|
||||
help='Maximum TLS version (default: 1.3)')
|
||||
parser.add_argument('--seclevel', type=int, default=None,
|
||||
help='OpenSSL @SECLEVEL to force (0 to enable NULL/3DES/legacy PSK)')
|
||||
parser.add_argument('--uri', default=None,
|
||||
help='X-Admin-Next-URI to hand the card (default: request URI)')
|
||||
parser.add_argument('--mode', choices=sorted(MODE_CONTENT_TYPE), default='ram',
|
||||
help='"ram" = GP Amendment B RAM to a SD (default), '
|
||||
'"rfm" = TS 102 226 RFM/RAM to the --targeted-application.')
|
||||
parser.add_argument('--targeted-application', default=None,
|
||||
help='X-Admin-Targeted-Application AID (hex). '
|
||||
'Required by --mode rfm, optional for --mode ram')
|
||||
parser.add_argument('--length-coding', choices=('definite', 'indefinite'), default='definite',
|
||||
help='Expanded format length coding "definite" "indefinite"')
|
||||
parser.add_argument('--apdu', action='append', default=[], metavar='HEX',
|
||||
help='one of many C-APDU (hex) to send, executed in order')
|
||||
parser.add_argument('--apdu-file', default=None,
|
||||
help='File with one C-APDU (hex) per line to push (# comments allowed)')
|
||||
parser.add_argument('--batch', action='store_true',
|
||||
help='All C-APDUs in one large command message')
|
||||
parser.add_argument('--raw-cmd', action='append', default=[], metavar='HEX',
|
||||
help='Debug, raw command')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='enable debug output')
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
|
||||
format='%(asctime)s %(levelname)s %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if args.mode == 'rfm' and not args.targeted_application:
|
||||
parser.error('--mode rfm requires --targeted-application <RFM Application AID>')
|
||||
content_type = MODE_CONTENT_TYPE[args.mode]
|
||||
|
||||
apdus: List[bytes] = [h2b(a) for a in args.apdu]
|
||||
if args.apdu_file:
|
||||
for line in Path(args.apdu_file).read_text().splitlines():
|
||||
line = line.split('#', 1)[0].strip()
|
||||
if line:
|
||||
apdus.append(h2b(line))
|
||||
command_bodies = build_command_bodies(apdus, batch=args.batch,
|
||||
length_coding=args.length_coding)
|
||||
command_bodies += [h2b(r) for r in args.raw_cmd]
|
||||
if not command_bodies:
|
||||
logger.warning('no C-APDUs: the server will answer the first POST with 204...')
|
||||
|
||||
targeted = format_aid(args.targeted_application) if args.targeted_application else None
|
||||
logger.info('mode=%s content-type=%s length-coding=%s targeted-application=%s',
|
||||
args.mode, content_type, args.length_coding, targeted or '(none)')
|
||||
|
||||
ssl_ctx = make_ssl_context(h2b(args.psk), args.psk_identity,
|
||||
ciphers=args.ciphers,
|
||||
min_tls=args.min_tls, max_tls=args.max_tls,
|
||||
seclevel=args.seclevel,
|
||||
identity_hint=args.psk_identity_hint,
|
||||
allow_any_identity=args.allow_any_identity)
|
||||
|
||||
server = Scp81AdminServer(args.host, args.port, ssl_ctx, command_bodies,
|
||||
next_uri=args.uri, targeted_application=targeted,
|
||||
content_type=content_type)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
logger.info('shutting down')
|
||||
server.shutdown()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""scp81_trigger.py -- build the OTA packet that asks the card to open an SCP81 admin session."""
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Eric Wild <ewild@sysmocom.de>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# Prints the apdu line for AdmSessTriggerParams TLV as the sms secured data, Expanded RFM mode,
|
||||
# to be fed into pysim_shell.py
|
||||
#
|
||||
# security params supplied either
|
||||
# - in the trigger
|
||||
# - from the cards data object,
|
||||
# trigger wins when both are present.
|
||||
# --no-sec omits them from the trigger so the stored ones are used.
|
||||
#
|
||||
# example params:
|
||||
# --psk-id 'PSK Identity 123' --kvn 0x41 --kid-ref 5
|
||||
# --ip 127.0.0.1 --port 8080 --buffer 512
|
||||
# --host 172.96.0.1 --uri '/server/adminagent?cmd=1'
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from osmocom.utils import b2h # noqa: E402
|
||||
from pySim.cat import (sms_pp_download_envelope, BearerDescription, # noqa: E402
|
||||
BufferSize, UiccTransportLevel, OtherAddress)
|
||||
from pySim.global_platform.http import (AdmSessTriggerParams, AdmSessionParams, # noqa: E402
|
||||
SecurityParams, HttpPostParams, RasConnectionParams,
|
||||
AdminHostParam, AdminUriParam)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--psk-id", help="PSK identity for ClientHello (required, unless --no-sec)")
|
||||
ap.add_argument("--kvn", type=lambda s: int(s, 0), help="key version of the PSK (required, unless --no-sec)")
|
||||
ap.add_argument("--kid-ref", type=lambda s: int(s, 0), help="PSK key id (required, unless --no-sec)")
|
||||
ap.add_argument("--host", help="HTTP Host header (required, unless --no-http)")
|
||||
ap.add_argument("--uri", help="HTTP request URI (required, unless --no-http)")
|
||||
ap.add_argument("--ip", help="administration server address, BIP (required, unless --no-conn)")
|
||||
ap.add_argument("--port", type=int, help="administration server port (required, unless --no-conn)")
|
||||
ap.add_argument("--buffer", type=int, help="BIP buffer size (required, unless --no-conn)")
|
||||
ap.add_argument("--no-conn", action="store_true", help="omit the connection params (tag 0x84)")
|
||||
ap.add_argument("--no-sec", action="store_true", help="omit the security params (tag 0x85)")
|
||||
ap.add_argument("--no-http", action="store_true", help="omit the HTTP POST params (tag 0x89)")
|
||||
|
||||
args = ap.parse_args()
|
||||
|
||||
missing = []
|
||||
if not args.no_conn:
|
||||
missing += [n for n in ('ip', 'port', 'buffer') if getattr(args, n) is None]
|
||||
if not args.no_sec:
|
||||
missing += [n for n in ('psk_id', 'kvn', 'kid_ref') if getattr(args, n) is None]
|
||||
if not args.no_http:
|
||||
missing += [n for n in ('host', 'uri') if getattr(args, n) is None]
|
||||
if missing:
|
||||
ap.error("pass every value required: %s." % " ".join("--" + n.replace('_', '-') for n in missing))
|
||||
|
||||
session = []
|
||||
if not args.no_conn:
|
||||
session.append(RasConnectionParams(children=[
|
||||
BearerDescription(decoded={'bearer_type': 'default', 'bearer_parameters': ''}),
|
||||
BufferSize(decoded=args.buffer),
|
||||
UiccTransportLevel(decoded={'protocol_type': 'tcp_uicc_client_remote',
|
||||
'port_number': args.port}),
|
||||
OtherAddress(decoded={'type_of_address': 'ipv4',
|
||||
'address': bytes(int(b) for b in args.ip.split("."))})]))
|
||||
if not args.no_sec:
|
||||
session.append(SecurityParams(decoded={'psk_id': args.psk_id.encode(), 'kvn': args.kvn,
|
||||
'kid': args.kid_ref, 'sha_type': None}))
|
||||
if not args.no_http:
|
||||
session.append(HttpPostParams(children=[AdminHostParam(decoded=args.host),
|
||||
AdminUriParam(decoded=args.uri)]))
|
||||
trig = AdmSessTriggerParams(children=[AdmSessionParams(children=session)]).to_tlv()
|
||||
|
||||
# stderr for logs, stdout for data
|
||||
print("# trigger TLV %d B %s" % (len(trig), trig.hex()), file=sys.stderr)
|
||||
print("# %-13s %d B %s" % ("secured data", len(trig), trig.hex()), file=sys.stderr)
|
||||
print(trig.hex())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+19
-77
@@ -24,8 +24,7 @@ import smpplib.gsm
|
||||
import smpplib.client
|
||||
import smpplib.consts
|
||||
import time
|
||||
from pySim.ota import OtaKeyset, OtaDialectSms, OtaAlgoCrypt, OtaAlgoAuth, OtaCheckError, CNTR_REQ, RC_CC_DS, POR_REQ
|
||||
from pySim.sms import ConcatenatedSmsReassembler
|
||||
from pySim.ota import OtaKeyset, OtaDialectSms, OtaAlgoCrypt, OtaAlgoAuth, CNTR_REQ, RC_CC_DS, POR_REQ
|
||||
from pySim.utils import b2h, h2b, is_hexstr
|
||||
from pathlib import Path
|
||||
|
||||
@@ -71,8 +70,6 @@ option_parser.add_argument("--por-req", choices=POR_REQ.decmapping.values(), def
|
||||
option_parser.add_argument('--src-addr', default='12', type=str, help='SMS source address (MSISDN)')
|
||||
option_parser.add_argument('--dest-addr', default='23', type=str, help='SMS destination address (MSISDN)')
|
||||
option_parser.add_argument('--timeout', default=10, type=int, help='Maximum response waiting time')
|
||||
option_parser.add_argument('--format', choices=['compact', 'expanded'], default='compact',
|
||||
help="Remote Application data format: 'compact' or 'expanded'")
|
||||
option_parser.add_argument('-a', '--apdu', action='append', required=True, type=is_hexstr, help='C-APDU to send')
|
||||
|
||||
class SmppHandler:
|
||||
@@ -80,8 +77,7 @@ class SmppHandler:
|
||||
|
||||
def __init__(self, host: str, port: int,
|
||||
system_id: str, password: str,
|
||||
ota_keyset: OtaKeyset, spi: dict, tar: bytes,
|
||||
remote_format: str = 'compact'):
|
||||
ota_keyset: OtaKeyset, spi: dict, tar: bytes):
|
||||
"""
|
||||
Initialize connection to SMPP server and set static OTA SMS-TPDU ciphering parameters
|
||||
Args:
|
||||
@@ -92,7 +88,6 @@ class SmppHandler:
|
||||
ota_keyset: OTA keyset to be used for SMS-TPDU ciphering
|
||||
spi: Security Parameter Indicator (SPI) to be used for SMS-TPDU ciphering
|
||||
tar: Toolkit Application Reference (TAR) of the targeted card application
|
||||
remote_format: Remote Application data format ('compact' or 'expanded', TS 102 226)
|
||||
"""
|
||||
|
||||
# Create and connect SMPP client
|
||||
@@ -108,58 +103,26 @@ class SmppHandler:
|
||||
self.ota_keyset = ota_keyset
|
||||
self.tar = tar
|
||||
self.spi = spi
|
||||
self.remote_format = remote_format
|
||||
self.reassembler = ConcatenatedSmsReassembler()
|
||||
|
||||
def __del__(self):
|
||||
if self.client:
|
||||
self.client.unbind()
|
||||
self.client.disconnect()
|
||||
|
||||
def _decode_resp(self, tpud: bytes) -> tuple:
|
||||
"""Decode a response SMS-TPDU into (response_packet, decoded).
|
||||
|
||||
Retry to decoding with ciphering disabled (in case the card has problems to decode the SMS-TDPU
|
||||
we have sent, the response will contain an unencrypted error message)
|
||||
"""
|
||||
try:
|
||||
return self.ota_dialect.decode_resp(self.ota_keyset, self.spi, tpud,
|
||||
remote_format=self.remote_format)
|
||||
except (ValueError, OtaCheckError):
|
||||
spi = self.spi.copy()
|
||||
spi['por_shall_be_ciphered'] = False
|
||||
spi['por_rc_cc_ds'] = 'no_rc_cc_ds'
|
||||
return self.ota_dialect.decode_resp(self.ota_keyset, spi, tpud,
|
||||
remote_format=self.remote_format)
|
||||
|
||||
def message_received_handler(self, pdu):
|
||||
if not pdu.short_message:
|
||||
return None
|
||||
logger.info("SMS-TPDU received: %s", b2h(pdu.short_message))
|
||||
tpud = self.reassembler.add(pdu.short_message)
|
||||
if tpud is None:
|
||||
logger.info("SMS-TPDU is part of concat message, waiting for more parts...")
|
||||
return None
|
||||
if tpud != pdu.short_message:
|
||||
logger.info("SMS-TPDU reassembled: %s", b2h(tpud))
|
||||
try:
|
||||
res, decoded = self._decode_resp(tpud)
|
||||
except Exception as e:
|
||||
# for example ENVELOPE POR
|
||||
logger.warning("Ignoring undecodable resp SMS-TPDU (%s: %s)", type(e).__name__, e)
|
||||
return None
|
||||
logger.info("SMS-TPDU decoded: %s", (res, decoded))
|
||||
# large app response as reassembled SEND SHORT MESSAGE, but
|
||||
# the ENVELOPE itself returns a POR without R-APDU.
|
||||
# smpplib poll() drains all pending SMS in one call, so that PoR is processed
|
||||
# right after the real response and would overwrite it,
|
||||
# which leaves transceive_apdu with no last_response_data to return.
|
||||
# Only allow a response that has no application data (decoded == None)
|
||||
# if we do not already have a real one.
|
||||
if decoded is None and self.response is not None and self.response[1] is not None:
|
||||
logger.info("ignoring status response to keep earlier app response")
|
||||
return None
|
||||
self.response = (res, decoded)
|
||||
if pdu.short_message:
|
||||
logger.info("SMS-TPDU received: %s", b2h(pdu.short_message))
|
||||
try:
|
||||
dec = self.ota_dialect.decode_resp(self.ota_keyset, self.spi, pdu.short_message)
|
||||
except ValueError:
|
||||
# Retry to decoding with ciphering disabled (in case the card has problems to decode the SMS-TDPU
|
||||
# we have sent, the response will contain an unencrypted error message)
|
||||
spi = self.spi.copy()
|
||||
spi['por_shall_be_ciphered'] = False
|
||||
spi['por_rc_cc_ds'] = 'no_rc_cc_ds'
|
||||
dec = self.ota_dialect.decode_resp(self.ota_keyset, spi, pdu.short_message)
|
||||
logger.info("SMS-TPDU decoded: %s", dec)
|
||||
self.response = dec
|
||||
return None
|
||||
|
||||
def message_sent_handler(self, pdu):
|
||||
@@ -220,14 +183,10 @@ class SmppHandler:
|
||||
tuple containing the last response data and the last status word as byte strings
|
||||
"""
|
||||
|
||||
if isinstance(apdu, (list, tuple)):
|
||||
logger.info("C-APDU(s) sending: %s...", [b2h(a) for a in apdu])
|
||||
else:
|
||||
logger.info("C-APDU sending: %s...", b2h(apdu))
|
||||
logger.info("C-APDU sending: %s...", b2h(apdu))
|
||||
|
||||
# translate to Secured OTA RFM
|
||||
secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu,
|
||||
remote_format=self.remote_format)
|
||||
secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu)
|
||||
# add user data header
|
||||
tpdu = b'\x02\x70\x00' + secured
|
||||
# send via SMPP
|
||||
@@ -241,17 +200,6 @@ class SmppHandler:
|
||||
container_dict = dict(container)
|
||||
resp = container_dict.get('last_response_data')
|
||||
sw = container_dict.get('last_status_word')
|
||||
# expanded format: decoded response carries
|
||||
# per command R-APDU list; log each one.
|
||||
for i, cmd in enumerate(container_dict.get('commands') or []):
|
||||
logger.info("R-APDU[%u] received: %s %s", i,
|
||||
cmd['response_data'], cmd['status_word'])
|
||||
if container_dict.get('truncated'):
|
||||
logger.warning("Response was TRUNCATED (SW 62F1): the card cut the response "
|
||||
"data short and did not execute the rest of the script")
|
||||
if container_dict.get('bad_format') is not None:
|
||||
logger.warning("Response contains a Bad format TLV: %s",
|
||||
container_dict['bad_format'])
|
||||
if resp is None:
|
||||
raise ValueError("Response does not contain any last_response_data, no R-APDU received!")
|
||||
if sw is None:
|
||||
@@ -285,14 +233,8 @@ if __name__ == '__main__':
|
||||
'por_shall_be_ciphered': not opts.por_no_ciphering,
|
||||
'por_rc_cc_ds': opts.por_rc_cc_ds,
|
||||
'por': opts.por_req}
|
||||
if opts.format == 'expanded':
|
||||
# TS 102 226 5.2.1.1: wrap each apdu in its own C-APDU TLV
|
||||
apdu = [h2b(a) for a in opts.apdu]
|
||||
else:
|
||||
# compact: C-APDUs are concatenated as single command string
|
||||
apdu = h2b("".join(opts.apdu))
|
||||
apdu = h2b("".join(opts.apdu))
|
||||
|
||||
smpp_handler = SmppHandler(opts.host, opts.port, opts.system_id, opts.password, ota_keyset, spi,
|
||||
h2b(opts.tar), remote_format=opts.format)
|
||||
smpp_handler = SmppHandler(opts.host, opts.port, opts.system_id, opts.password, ota_keyset, spi, h2b(opts.tar))
|
||||
resp, sw = smpp_handler.transceive_apdu(apdu, opts.src_addr, opts.dest_addr, opts.timeout)
|
||||
print("%s %s" % (b2h(resp), b2h(sw)))
|
||||
|
||||
@@ -14,8 +14,6 @@ import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
sys.path.insert(0, os.path.abspath('.')) # for local extensions (pysim_fs_sphinx, ...)
|
||||
sys.path.insert(0, os.path.abspath('../contrib/rcp')) # for argparse
|
||||
sys.path.insert(0, os.path.abspath('../contrib/rcp/usage_example')) # for argparse
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
@@ -44,7 +42,6 @@ extensions = [
|
||||
"sphinx.ext.autosectionlabel",
|
||||
"sphinx.ext.napoleon",
|
||||
"pysim_fs_sphinx",
|
||||
"sphinx.ext.graphviz",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
|
||||
@@ -50,7 +50,6 @@ pySim consists of several parts:
|
||||
suci-keytool
|
||||
saip-tool
|
||||
smpp-ota-tool
|
||||
rcpf
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
||||
-690
@@ -1,690 +0,0 @@
|
||||
Remote Card Procedure Framework
|
||||
===============================
|
||||
|
||||
The Remote Card Procedure Framework `(RCPF)` is a modular system to provide
|
||||
custom, remote controlled, procedures to card `(UICC or eUICC)` holders. The
|
||||
card holder uses a minimal client program `(RCP Client)` together with a PC/SC
|
||||
reader. The client program will then connect to a remote server `(RCP Server)`.
|
||||
The remote server maintains connections to custom modules `(RCP Modules)`, where
|
||||
each module implements a set of procedures (commands). Based on an internal list,
|
||||
the remote server will offer a set of suitable commands to the client. The card
|
||||
holder may then chose a command to request the execution of a specific remote
|
||||
card procedure. The server will make the connection to the matching module and
|
||||
act as a proxy between the module and the client program.
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph foo {
|
||||
|
||||
subgraph cluster_server {
|
||||
label = "server (card issuer)"
|
||||
RCPS [label = "RCP Server"];
|
||||
RCPM [label = "RCP Module"];
|
||||
CKP [label = "CardKeyProvider"];
|
||||
}
|
||||
|
||||
subgraph cluster_field {
|
||||
label = "field (card holder)"
|
||||
ICC [label = "UICC/eUICC"];
|
||||
RCPC [label = "RCP Client"];
|
||||
}
|
||||
|
||||
RCPC -> ICC [label="PC/SC, APDU"];
|
||||
RCPC -> RCPS [label="WS, JSON"];
|
||||
RCPS -> CKP [label="pgSQL or CSV"];
|
||||
RCPS -> RCPM [label="WS, JSON", headlabel="n", taillabel="1", dir=both];
|
||||
|
||||
}
|
||||
|
||||
in case the procedure requires a secure channel, the key material is retrieved
|
||||
using a `CardKeyProvider` [1]. Since the retrieval of the key material
|
||||
as well as the secure channel establishment happens internally, the related
|
||||
key material is never disclosed to the client side.
|
||||
|
||||
This solves a major problem many card deployments suffer from: Due to security
|
||||
reasons it is not always be possible to disclose key material to the card
|
||||
holder. This becomes a problem in case card contents have to be modified after
|
||||
the card had been deployed. This often means that the card issuer has to
|
||||
physically replace the already deployed cards. With `RCPF`, the card issuer can
|
||||
replace this process by deploying a suitable `RCP Module` on his server to offer
|
||||
a fix-up procedure that the card holder can call remotely.
|
||||
|
||||
[1] :ref:`Retrieving card-individual keys via CardKeyProvider`
|
||||
|
||||
In the following we will describe the system components in further detail. We
|
||||
will also give an introduction on how to implement custom `RCP Modules`
|
||||
|
||||
RCP Server
|
||||
~~~~~~~~~~
|
||||
|
||||
The `RCP Server` is the core component in the overall system. It acts as a proxy
|
||||
between the `RCP Modules` (see below) and the `RCP Client` (see below). The
|
||||
`RCP Server` is permanently aware of which `RCP Modules` are available and knows
|
||||
their properties. With this knowledge, the `RCP Server` is able to check which
|
||||
module provides suitable procedures for a specific card type.
|
||||
|
||||
Another responsibility of the `RCP Server` is to retrieve the key material using
|
||||
the `CardKeyProvider`. As far as the `CardKeyProvider` is concerned, the RCP
|
||||
Server takes the exact same commandline options as `pySim-shell.py`. However, in
|
||||
case column encryption is used. The decryption key shall be passed to the
|
||||
`RCP Module` instead to the `RCP Server`. This moves the decryption to the point
|
||||
where the key material is actually needed.
|
||||
|
||||
To ensure the privacy of the traffic exchanged between `RCP Client`,
|
||||
`RCP Server` and the `RCP Modules`, all links use SSL/TLS encrypted channels.
|
||||
This is in particular relevant for the `RCP Client` which usually connects to
|
||||
the `RCP Server` via the public internet.
|
||||
|
||||
Since the `RCP Server` is exposed to the public internet, it also requires some
|
||||
level of protection against malicious requests. To minimize the risk arising
|
||||
from malformed requests, each incoming and outgoing message is validated against
|
||||
a JSON schema (also on the internal interfaces). Incoming requests from the
|
||||
`RCP Client` side are also rate-limited to guard against excessive requests
|
||||
(DoS).
|
||||
|
||||
To monitor the `RCP Client` requests, the `RCP Server` supports logging to an
|
||||
`OpenObserve` monitoring entity. For each request exactly one report es
|
||||
generated and sent to `OpenObserve`. For successful request, this report will
|
||||
only contain metadata. In case of crashes or when the return code of the
|
||||
`RCP Module` procedure is not 0, a full debug log is included as well.
|
||||
|
||||
.. argparse::
|
||||
:module: contrib.rcp.rcp_server
|
||||
:func: option_parser
|
||||
:prog: contrib/rcp/rcp_server.py
|
||||
|
||||
|
||||
RCP Client
|
||||
~~~~~~~~~~
|
||||
|
||||
The `RCP Client` is used in the field by the card holder to request command
|
||||
lists and to request the execution of procedures from the `RCP Server`.
|
||||
|
||||
The execution of a procedure is usually done in two steps. In the first step,
|
||||
the card holder will request a list with available commands using the `--help`
|
||||
option. The command list is then requested from the `RCP Server` displayed as
|
||||
a regular commandline help-screen. The list will only contain commands, which
|
||||
are actually suitable for the specific card type/model that card holder owns.
|
||||
|
||||
In the second step, the card holder will choose a command to request the
|
||||
execution of the related procedure. In case the user already knows exactly
|
||||
which command to execute, the first step may also be skipped. The request of
|
||||
command lists for the purpose of displaying commandline help-screens is
|
||||
entirely optional.
|
||||
|
||||
To avoid having to upgrade the `RCP Client` too often, the implementation is kept
|
||||
as simple as possible. Technically, the RCP Client is not much more than a
|
||||
proxy between a PC/SC-Reader and the `RCP Server`. All higher level tasks, like
|
||||
requesting the ICCID (UICC or eSIM) or the EID (eUICC) are implemented on the
|
||||
server side.
|
||||
|
||||
.. argparse::
|
||||
:module: contrib.rcp.rcp_client
|
||||
:func: option_parser
|
||||
:prog: contrib/rcp/rcp_client.py
|
||||
|
||||
|
||||
RCP Module
|
||||
~~~~~~~~~~
|
||||
|
||||
The processing chain terminates at one of multiple `RCP Modules`. The `RCP Module`
|
||||
is the custom implementation that implements one or more procedures. The
|
||||
framework is designed in such a way that `RCP Modules` have minimal boilerplate
|
||||
code. The implementation is kept simple. Users, which are familiar with
|
||||
`pySim-shell.py` and its API will find the implementation of custom `RCP Modules`
|
||||
as simple as implementing a new `pySim-shell.py` command.
|
||||
|
||||
From inside a procedure, the API user has access to the same objects (rs, card,
|
||||
lchan) that are also usually available in `pySim-shell.py` environment.
|
||||
|
||||
To reset the card, retrieve the ATR and to exchange APDUs, the `pySim.transport`
|
||||
API together with a custom `LinkBase (RcpsSimLink)` object is used. This means
|
||||
that all modules which depend on the `pySim.transport` API can be used without
|
||||
modification.
|
||||
|
||||
A procedure always runs in a dedicated thread, which means no special
|
||||
precautions are necessary. A procedure may wait or sleep without disturbing
|
||||
other requests.
|
||||
|
||||
Even though there are similarities to `pySim-shell` one has to keep in mind that
|
||||
`RCP Modules` are intended to run non-interactively, which means they naturally
|
||||
do not provide any support for `cmd2` API calls. This means that before code
|
||||
from `pySim-shell` commands can be re-used, any `cmd2` entanglement must be
|
||||
removed or separated otherwise.
|
||||
|
||||
.. argparse::
|
||||
:module: contrib.rcp.usage_example.rcp_module
|
||||
:func: option_parser
|
||||
:prog: contrib/rcp/usage_example/rcp_module.py
|
||||
|
||||
|
||||
Usage Example
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
All system components and related modules can be found in `contrib/rcp`. The
|
||||
sub directory `usage_example` contains an example `RCP Module` and scripts to
|
||||
make it easier to get started. The following steps explain in detail how to get
|
||||
the `usage_example` running.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
The `usage_example` contains a file `params.cfg`. This file contains variables,
|
||||
which hold the parameters for the shell-scripts included in the example. The
|
||||
parameters set up the system in such a way that everything runs locally.
|
||||
Normally no changes are required, but it is strongly advised to review the
|
||||
parameters to verify there are no clashes with other services.
|
||||
|
||||
Preparing Card Keys
|
||||
-------------------
|
||||
|
||||
The example assumes a PC/SC reader and a `sysmoISIM-SJA5` or similar. To run
|
||||
the `usage_example`, no modification to the card itself are required, but the
|
||||
example key material (SCP02) in `card_data.csv` must match the test card.
|
||||
|
||||
The following example assumes that the card has the ICCID ``8949440000001155306``
|
||||
and the following SCP02 keys:
|
||||
|
||||
+---------+----------------------------------+
|
||||
| Keyname | Keyvalue |
|
||||
+=========+==================================+
|
||||
| ENC/KIC | F09C43EE1A0391665CC9F05AF4E0BD10 |
|
||||
+---------+----------------------------------+
|
||||
| MAC/KID | 01981F4A20999F62AF99988007BAF6CA |
|
||||
+---------+----------------------------------+
|
||||
| DEK/KIK | 8F8AEE5CDCC5D361368BC45673D99195 |
|
||||
+---------+----------------------------------+
|
||||
|
||||
This would result into a `card_data.csv` file with the following content:
|
||||
|
||||
::
|
||||
|
||||
iccid,kic,kid,kik
|
||||
8949440000001155306,F09C43EE1A0391665CC9F05AF4E0BD10,01981F4A20999F62AF99988007BAF6CA,8F8AEE5CDCC5D361368BC45673D99195
|
||||
|
||||
|
||||
See also: :ref:`Retrieving card-individual keys via CardKeyProvider` and :ref:`Guide: Managing GP Keys`
|
||||
|
||||
When `card_data.csv` is re-aligned, the columns containing key material need to
|
||||
be encrypted. This is done by running `encrypt_card_data.sh`. This script will
|
||||
output a file `card_data.csv.encr` which contains the encrypted key material.
|
||||
|
||||
Running the RCP Server
|
||||
----------------------
|
||||
|
||||
The `RCP Server` can be started using the included `start_rcp_server.sh` script.
|
||||
|
||||
::
|
||||
|
||||
$ ./start_rcp_server.sh
|
||||
+ PYTHONPATH=../../../
|
||||
+ ../../..//contrib/rcp/rcp_server.py --rcpc-server-addr 127.0.0.1 --rcpc-server-port 8000 --rcpc-server-cert ./certs/example_ssl_rcpc_rcps_cert.pem --rcpm-server-addr 127.0.0.1 --rcpm-server-port 8010 --rcpm-server-cert ./certs/example_ssl_rcpm_rcps_cert.pem --rcpm-module-ca-cert ./certs/example_ssl_rcp_ca_cert.crt --csv ./card_data.csv.encr
|
||||
INFO: loading SSL/TLS CA certificate (RCP Module Command Server Client): ./certs/example_ssl_rcp_ca_cert.crt
|
||||
INFO: Using CSV file as card key data source: ./card_data.csv.encr
|
||||
WARNING: Reporting to OpenObserve: (disabled)
|
||||
INFO: Rate-Limit: max 10 requests per sec.
|
||||
INFO: RCP Client Server at: 127.0.0.1:8000
|
||||
INFO: RCP Module server at: 127.0.0.1:8010
|
||||
|
||||
We can see that now to ports have been opened. `127.0.0.1:8000` is the port
|
||||
where `RCP Clients` can connect. In a productive setup, this port would
|
||||
normally be reachable from outside. The other port on `127.0.0.1:8010` is
|
||||
accepting connections from `RCP Modules` This port should not be reachable
|
||||
from the outside. It is intended to be used for the interprocess communication
|
||||
between the `RCP Server` and the `RCP Modules`
|
||||
|
||||
In this state, the `RCP Server` waits for requests from both `RCP Clients` and
|
||||
`RCP Modules`. However, there are not `RCP Modules` registered yet, so any
|
||||
request from an `RCP Client` would be quilted with an error message.
|
||||
|
||||
Running the RCP Module
|
||||
----------------------
|
||||
|
||||
For a functioning setup a suitable `RCP Module` is needed. The provided
|
||||
`rcp_module.py` python program implements a few procedures which are suitable
|
||||
for a `sysmoISIM-SJA5` card.
|
||||
|
||||
We can start the `RCP Module` with the provided start script
|
||||
`start_rcp_module.sh`
|
||||
|
||||
::
|
||||
|
||||
$ ./start_rcp_module.sh
|
||||
+ PYTHONPATH=../../../:../../..//contrib/rcp
|
||||
+ ./rcp_module.py --uri wss://127.0.0.1:8010 --rcps-ca-cert ./certs/example_ssl_rcp_ca_cert.crt --rcpm-cmd-server-addr 127.0.0.1 --rcpm-cmd-server-port 8020 --rcpm-cmd-server-cert ./certs/example_ssl_rcps_rcpm_cert.pem --column-key kic:00112233445566778899AABBCCDDEEFF --column-key kid:00112233445566778899AABBCCDDEEFF --column-key kik:00112233445566778899AABBCCDDEEFF
|
||||
INFO: RCP Module startup: rcp_module
|
||||
INFO: loading SSL/TLS CA certificate (RCPM Server Client): ./certs/example_ssl_rcp_ca_cert.crt
|
||||
INFO: RCPC command server at: 127.0.0.1:8020
|
||||
|
||||
The `RCP Module` is now connected to the `RCP Server`. The log output of the
|
||||
`RCP Server` also confirms that there is a new `RCP Module` available.
|
||||
|
||||
::
|
||||
|
||||
INFO: new RCP module, RCP modules available: 'rcp_module'
|
||||
|
||||
On the output of the `RCP Module` we can see that the `RCP Module` has
|
||||
opened another port on `127.0.0.1:8020`. This is where the `RCP Module` accepts
|
||||
dedicated connections from the `RCP Server` when an `RCP Client` requests a
|
||||
procedure. In an installation with multiple `RCP Modules`, each `RCP Module`
|
||||
must use a dedicated port number.
|
||||
|
||||
Note that we also pass the column key for the key material using the
|
||||
`--column-key` parameter. This parameter works exactly as in `pySim-shell`.
|
||||
We supply the column key to the `RCP Module` and not to the `RCP Server`
|
||||
move the decryption as close as possible to where it is needed.
|
||||
|
||||
|
||||
Running the RCP Client
|
||||
----------------------
|
||||
|
||||
The `usage_example` provides a shell-script `run_rcp_client.sh` that which
|
||||
requests commandline help and requests procedures by calling other scripts.
|
||||
However to get an understanding on how the `RCP Client` is supposed to be used,
|
||||
it makes more sense to call the sub scripts individually. We will now go through
|
||||
step by step.
|
||||
|
||||
The first shell-script `./run_rcp_client_help.sh` assumes that the card holder
|
||||
uses the `RCP Client` for the first time. He does not know which commandline
|
||||
arguments are available, so he just calls `rcp_client.py` with the option `-h`.
|
||||
|
||||
::
|
||||
|
||||
$ ./run_rcp_client_help.sh
|
||||
+ PYTHONPATH=../../../
|
||||
+ ../../..//contrib/rcp/rcp_client.py -h
|
||||
usage: rcp_client.py [-h] [-d DEV] [-b BAUD] [--pcsc-shared] [-p PCSC | --pcsc-regex REGEX] [--modem-device DEV] [--modem-baud BAUD] [--osmocon PATH]
|
||||
[--apdu-trace] [--verbose] [--uri URI] [--ca-cert CA_CERT]
|
||||
|
||||
RCP Client
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--apdu-trace Trace the command/response APDUs exchanged with the card (default: False)
|
||||
--verbose Enable verbose logging (default: False)
|
||||
--uri URI URI of the RCP-Server (default: None)
|
||||
--ca-cert CA_CERT SSL/TLS CA-Certificate of the RCP-Server (default: None)
|
||||
...
|
||||
|
||||
PC/SC Reader:
|
||||
Use a PC/SC card reader to talk to the SIM card. PC/SC is a standard API for how applications access smart card readers, and is available on a variety of
|
||||
operating systems, such as Microsoft Windows, MacOS X and Linux. Most vendors of smart card readers provide drivers that offer a PC/SC interface, if not even
|
||||
a generic USB CCID driver is used. You can use a tool like ``pcsc_scan -r`` to obtain a list of readers available on your system.
|
||||
|
||||
--pcsc-shared Open PC/SC reaer in SHARED access (default: EXCLUSIVE) (default: False)
|
||||
-p, --pcsc-device PCSC
|
||||
Number of PC/SC reader to use for SIM access (default: None)
|
||||
--pcsc-regex REGEX Regex matching PC/SC reader to use for SIM access (default: None)
|
||||
...
|
||||
|
||||
|
||||
From the output the card holder learns that there is an `--uri` parameter and
|
||||
that the same PC/SC options like in `pySim-shell.py` are supported. There is
|
||||
also a `--ca-cert` parameter where a CA certificate can be supplied in case the
|
||||
`RCP Server` uses a self-signed CA (which applies to this example)
|
||||
|
||||
The second script `run_rcp_client_help_cmd.sh` assumes that the card holder now
|
||||
knows that the minimum required parameters are the `--uri` of the `RCP Server`,
|
||||
the `--ca-cert` of the `RCP Server` and `-p` to tell the `RCP Client` which PC/SC
|
||||
reader to use.
|
||||
|
||||
::
|
||||
|
||||
$ ./run_rcp_client_help_cmd.sh
|
||||
+ PYTHONPATH=../../../
|
||||
+ ../../..//contrib/rcp/rcp_client.py --uri wss://127.0.0.1:8000 --ca-cert ./certs/example_ssl_rcp_ca_cert.crt -p 0 -h
|
||||
INFO: loading SSL/TLS CA certificate (RCP Server CA): ./certs/example_ssl_rcp_ca_cert.crt
|
||||
INFO: Using reader PCSC[Alcor Micro AU9540 00 00]
|
||||
INFO: Detected Card with ATR: 3B9F96801F878031E073FE211B674A357530350265F8
|
||||
INFO: RCP Server URI: wss://127.0.0.1:8000
|
||||
INFO: Checking version ...
|
||||
INFO: RCP Client version: software=1.0.0, protocol=1.0.0
|
||||
INFO: RCP Server version: software=1.0.0, protocol=1.0.0
|
||||
INFO: Requesting module descriptions from RCP Server ...
|
||||
usage: rcp_client.py [-h] [-d DEV] [-b BAUD] [--pcsc-shared] [-p PCSC | --pcsc-regex REGEX] [--modem-device DEV] [--modem-baud BAUD] [--osmocon PATH]
|
||||
[--apdu-trace] [--verbose] [--uri URI] [--ca-cert CA_CERT]
|
||||
{rcp_module_reset,rcp_module_read_binary,rcp_module_read_record,rcp_module_unlock_aram} ...
|
||||
|
||||
RCP Client
|
||||
|
||||
positional arguments:
|
||||
{rcp_module_reset,rcp_module_read_binary,rcp_module_read_record,rcp_module_unlock_aram}
|
||||
RCP command to use
|
||||
rcp_module_reset reset the card
|
||||
rcp_module_read_binary
|
||||
read binary data from a transparent file.
|
||||
rcp_module_read_record
|
||||
read binary data from a transparent file.
|
||||
rcp_module_unlock_aram
|
||||
unlock a locked ARA-M applet on a sysmoISIM-SJA5
|
||||
...
|
||||
|
||||
The help screen now shows additional positional arguments. Those positional
|
||||
arguments are the commands which the card holder can use to request a
|
||||
procedure. In this example we have four procedures we can call:
|
||||
`rcp_module_reset`, `rcp_module_read_binary`, `rcp_module_read_record`,
|
||||
and `rcp_module_unlock_aram`
|
||||
|
||||
In the log output above the help screen, we can also see that a connection was
|
||||
made and that the `RCP Client` has requested module descriptions from the
|
||||
server. The `RCP Client` has sent the ATR of the card to the `RCP Server`. The
|
||||
`RCP Server` has used this information to look through its internal list to
|
||||
find modules which offer procedures suitable for this specific card.
|
||||
|
||||
The card holder now knows which commands or procedures are available, but he
|
||||
still does not know if arguments are required and what those arguments are.
|
||||
The third script `run_rcp_client_help_cmd_specific.sh` shows how the card
|
||||
holder can request a dedicated help-screen for each of the commands.
|
||||
|
||||
::
|
||||
|
||||
$ ./run_rcp_client_help_cmd_specific.sh
|
||||
...
|
||||
+ PYTHONPATH=../../../
|
||||
+ ../../..//contrib/rcp/rcp_client.py --uri wss://127.0.0.1:8000 --ca-cert ./certs/example_ssl_rcp_ca_cert.crt -p 0 rcp_module_read_record --help
|
||||
INFO: loading SSL/TLS CA certificate (RCP Server CA): ./certs/example_ssl_rcp_ca_cert.crt
|
||||
INFO: Using reader PCSC[Alcor Micro AU9540 00 00]
|
||||
INFO: Detected Card with ATR: 3B9F96801F878031E073FE211B674A357530350265F8
|
||||
INFO: RCP Server URI: wss://127.0.0.1:8000
|
||||
INFO: Checking version ...
|
||||
INFO: RCP Client version: software=1.0.0, protocol=1.0.0
|
||||
INFO: RCP Server version: software=1.0.0, protocol=1.0.0
|
||||
INFO: Requesting module descriptions from RCP Server ...
|
||||
usage: rcp_client.py rcp_module_read_record [-h] --fid FID --record RECORD
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--fid FID File identifier to of the file to read
|
||||
--record RECORD File record to read
|
||||
...
|
||||
|
||||
We can see in the log that the `RCP Client` again sends a request to the
|
||||
`RCP Server` and retrieves the `RCP Module` descriptions. Then a dedicated
|
||||
help-screen for the `rcp_module_read_record` command is displayed. Now the card
|
||||
holder knows which parameters are required to perform the related procedure.
|
||||
|
||||
Until this point there was only interaction with the `RCP Client` and the
|
||||
`RCP Server`. The `RCP Module` has not seen any requests yet. The provided
|
||||
script `run_rcp_client_cmd.sh` illustrates how the card holder can run an
|
||||
command that performs an actual procedure with the `RCP Module`.
|
||||
|
||||
::
|
||||
|
||||
$ ./run_rcp_client_cmd.sh
|
||||
...
|
||||
+ PYTHONPATH=../../../
|
||||
+ ../../..//contrib/rcp/rcp_client.py --uri wss://127.0.0.1:8000 --ca-cert ./certs/example_ssl_rcp_ca_cert.crt -p 0 rcp_module_read_record --fid 3f00 --fid 2f00 --record 1
|
||||
INFO: loading SSL/TLS CA certificate (RCP Server CA): ./certs/example_ssl_rcp_ca_cert.crt
|
||||
INFO: Using reader PCSC[Alcor Micro AU9540 00 00]
|
||||
INFO: Detected Card with ATR: 3B9F96801F878031E073FE211B674A357530350265F8
|
||||
INFO: RCP Server URI: wss://127.0.0.1:8000
|
||||
INFO: Checking version ...
|
||||
INFO: RCP Client version: software=1.0.0, protocol=1.0.0
|
||||
INFO: RCP Server version: software=1.0.0, protocol=1.0.0
|
||||
INFO: Requesting module descriptions from RCP Server ...
|
||||
INFO: Executing command with RCP Server ...
|
||||
INFO: RcpcCltConnHdlr(140335960510480) -- reading linear-fixed file: ['3f00', '2f00'] ...
|
||||
INFO: RcpcCltConnHdlr(140335960510480) -- file content is: 61294F10A0000000871002FFFFFFFF890709000050055553696D31730EA00C80011781025F608203454150
|
||||
INFO: Command execution done, rc: 0
|
||||
|
||||
The example reads record 1 from the file ``3F00/2F00`` and returns the file
|
||||
content. We also can see by the return code that the procedure was successful.
|
||||
The return code is also passed to `sys.exit()`, so that the card holder can
|
||||
use it in a script.
|
||||
|
||||
The APDUs required to perform this action were entirely generated under the
|
||||
control of the `RCP Module`. In the log of the `RCP Server` we can see which
|
||||
command was executed on which `RCP Module` was used. We also see the return
|
||||
code here as well.
|
||||
|
||||
::
|
||||
|
||||
...
|
||||
INFO: RcpcSrvConnHdlr(140093766623552) -- executing procedure for command "rcp_module_read_record" on module "rcp_module" at: wss://127.0.0.1:8020
|
||||
INFO: RcpcSrvConnHdlr(140093766623552) -- command execution done, rc: 0
|
||||
...
|
||||
|
||||
In the log of the `RCP Module` we can follow up on how the procedure was
|
||||
carried out.
|
||||
|
||||
::
|
||||
|
||||
...
|
||||
INFO: RcpmCmdSrvConnHdlr(140156091028880) -- executing command: rcp_module_read_record ['--fid', '3f00', '--fid', '2f00', '--record', '1']
|
||||
INFO: Waiting for card...
|
||||
INFO: Card is of type: UICC
|
||||
INFO: Detected UICC Add-on "SIM"
|
||||
INFO: Detected UICC Add-on "GSM-R"
|
||||
INFO: Detected UICC Add-on "RUIM"
|
||||
WARNING: EF.DIR seems to be empty!
|
||||
INFO: ADF.ISD: a000000003000000
|
||||
INFO: ARA-M: a00000015141434c00
|
||||
INFO: ISIM: a0000000871004
|
||||
INFO: USIM: a0000000871002
|
||||
INFO: Detected CardModel: SysmocomSJA5
|
||||
INFO: RcpmCmdSrvConnHdlr(140156091028880) -- reading linear-fixed file: ['3f00', '2f00'] ...
|
||||
INFO: RcpmCmdSrvConnHdlr(140156091028880) -- file content is: 61294F10A0000000871002FFFFFFFF890709000050055553696D31730EA00C80011781025F608203454150
|
||||
INFO: RcpmCmdSrvConnHdlr(140156091028880) -- command execution done, rc: 0
|
||||
...
|
||||
|
||||
In first line we see the command and its parameters. The lines that follow will
|
||||
look familiar to `pySim-shell` users. The last three log lines carry the print
|
||||
statements which we also see in the log messages on the `RCP Client`. The last
|
||||
line informs about the conclusion of the procedure and also shows the return
|
||||
code.
|
||||
|
||||
|
||||
Implementing an RCP Module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To make use of the Remote Card Procedure Framework, it is eventually necessary
|
||||
to implement a custom `RCP Module`. In the following section, we will go
|
||||
through the implementation of the `RCP Module` that is provided with the
|
||||
`usage_example`.
|
||||
|
||||
NOTE: much of the following is explained in greater detail in the comments
|
||||
found in `rcp_module_utils.py`.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
`RCP Modules` are normal python programs that can started directly from the
|
||||
command prompt. However, due to the location of the file it is necessary that
|
||||
`PYTHONPATH` points to the location of the `pySim` modules as well as to the
|
||||
modules found in `contrib/rcp` (see `start_rcp_module.sh` for reference).
|
||||
|
||||
As mentioned earlier `RCP Modules` may use the `pySim` API like any other
|
||||
`pySim` program, given that there is no dependency to `cmd2`. So it is no
|
||||
surprise that we find some `pySim` modules in the import section of the
|
||||
provided example.
|
||||
|
||||
The utilities required to implement an `RCP Module` are imported from
|
||||
`rcp_module_utils.py`. From this module we import two functions
|
||||
`rcpm_setup_argparse` and `rcpm_run_module` and the two classes `RCP Module`
|
||||
and `RcpModuleHdlr`.
|
||||
|
||||
Function: rcpm_setup_argparse
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The first function `rcpm_setup_argparse` returns an argument parser that is already
|
||||
equipped with the basic commandline arguments that an `RCP Module` needs. In
|
||||
case the specific `RCP Module` implementation requires additional arguments,
|
||||
those can be added using normal `argparse` API calls.
|
||||
|
||||
Function: rcpm_run_module
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The second function `rcpm_run_module` is used to run the `RCP Module`. This
|
||||
function gets the parsed commandline options (`opts`) and the `RcpModule` class
|
||||
(`module`) as parameters. In addition to that, `rcpm_run_module` also accepts
|
||||
custom `*args` and `**kwargs` arguments, which are passed to the constructor of
|
||||
the `RcpModule` class.
|
||||
|
||||
When `rcpm_run_module` is called. It registers the `RCP Module` and starts the
|
||||
RCP Client command server. It also takes care of the proper instantiation of the
|
||||
`RcpModule` class, which were passed with the `module` parameter.
|
||||
|
||||
Class: RcpModule
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
The Class `RcpModule` is the base class that is used to create a concrete
|
||||
`RCP Module` implementaion. Through this class, the API user defines the
|
||||
properties of the `RCP Module` as well as the command methods, which implement
|
||||
the related `Remote Card Procedures`.
|
||||
|
||||
Class: RcpModuleHdlr
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The class `RcpModuleHdlr` is used by the framework to instantiate a handler
|
||||
object (`hdlr`), which is passed to each of the aforementioned command methods.
|
||||
The handler object is used as a vehicle to provide access the resources we need
|
||||
to send APDUs, print messages on the `RCP Client`, etc.
|
||||
|
||||
Module Properties
|
||||
-----------------
|
||||
|
||||
Before we can define any module properties, we first need to create a derived
|
||||
class from the `RcpModule` class we have imported from `rcp_module_utils.py`.
|
||||
In that class, we then define the basic properties of the `RCP Module`.
|
||||
|
||||
name
|
||||
^^^^
|
||||
|
||||
Each `RCP Module` needs a distinct name. The name must not collide with the
|
||||
names of other `RCP Modules`. The name uniquely identifies the `RCP Module` and
|
||||
is used as a prefix for the command names used with the user interface of the
|
||||
`RCP Client`. Therefore a short name is desirable.
|
||||
|
||||
cmd_descr
|
||||
^^^^^^^^^
|
||||
|
||||
The `cmd_descr` property defines the command properties. Since an `RCP Module`
|
||||
may offer multiple commands (procedures), this property is an array, where
|
||||
each item holds the definition for one specific command.
|
||||
|
||||
The command definitions are formatted as a python dict. Like the `RCP Module`
|
||||
itself, each command has a `name`. As mentioned before. This name is concatenated
|
||||
with the name of the `RCP Module`.
|
||||
|
||||
Each command definition also gets a `help` string. The help string will show up
|
||||
in the commandline help of the `RCP Client`. It should be short and concise.
|
||||
|
||||
Command definitions also need to define commandline arguments. For this an
|
||||
`args` array is added to the command definition as well. In case no arguments
|
||||
are provided. The array is empty. Otherwise it will contain one or more dict
|
||||
members, where each specifies a `name` and a `spec`. The `name` sets the
|
||||
argument name (e.g. --fid), and the `spec` specifies the properties of the
|
||||
argument. The concept is borrowed from `argparse` and works very similar. API
|
||||
users can specify `required`, `help`, `default` and a type. However, to avoid
|
||||
name-space collisions, the type field is called `pytype` and the type identifier
|
||||
must be passed as a string (e.g. 'int').
|
||||
|
||||
In case a procedure requires key material from the `CardKeyProvider`, the API
|
||||
user may add a `get_keys` field to the command definition. In case eUICC keys
|
||||
are needed. The API user will add a dict member with key `euicc` and populate
|
||||
the value with an array that holds the column names of the columns where the
|
||||
keys are found. The same also works for UICC keys by using 'uicc' as dict key.
|
||||
When `get_keys` is correctly populated and the correct column keys are supplied
|
||||
to the `RCP Module` at runtime. The `RCP Framework` will automatically retrieve
|
||||
the key material, decrypt it and make it available to the related command
|
||||
method.
|
||||
|
||||
suitable_for
|
||||
^^^^^^^^^^^^
|
||||
|
||||
`suitable_for` is the third and last property, the API user must define. This
|
||||
property holds an array where each member is a dict that defines a distinct
|
||||
property of the card for which the module is suitable for. The `RCP Server`
|
||||
uses this information to see which modules are suitable for a specific request.
|
||||
As of now, the only property we can use to make the distinction, is the ATR of
|
||||
the card.
|
||||
|
||||
Custom Resources
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
In case an `RCP Module` requires custom resources, those may be initialized using
|
||||
a custom constructor in the `RCP Module` class derived from `RcpModule`. This
|
||||
constructor receives the `*args` and `**kwargs` arguments passed to
|
||||
`rcpm_run_module`. However, this is an optional step. In case no constructor is
|
||||
defined, the default constructor is used.
|
||||
|
||||
In addition to that, the API user may also define additional properties and
|
||||
methods, provided they do not collide with existing methods of the base class.
|
||||
|
||||
Command Methods
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Command methods are essentially normal python methods. However, since those
|
||||
methods are called by the `RCP Framework`, they must follow a distinct scheme,
|
||||
which we will go through in the following.
|
||||
|
||||
Each command defined in `cmd_descr` requires a corresponding command method. A
|
||||
command method is always prefixed with `cmd_`. Then the exact name of the
|
||||
command follows as defined in `cmd_descr`. For example if we have defined a
|
||||
command with the name `read_record`, we must also define a method with the name
|
||||
`cmd_read_record`.
|
||||
|
||||
The parameter list of a command method always contains only `self` and `hdlr`.
|
||||
The `hdlr` parameter is the handler object (`RcpModuleHdlr`) through which we
|
||||
access the resources provided by the `RCP Framework`.
|
||||
|
||||
Inside a command method, the API user is free to perform any task he wants.
|
||||
Command Methods always run in a dedicated thread and may sleep or wait at any
|
||||
time without disturbing running procedures from other requestors.
|
||||
|
||||
A command method should always return an integer as return code. In case the
|
||||
procedure ends successfully, the return code shall be `0`. The return code is
|
||||
passed through to the `RCP Client`, which returns it on exit to the operating
|
||||
system.
|
||||
|
||||
|
||||
Handler Resources
|
||||
-----------------
|
||||
|
||||
As mentioned earlier, a commend method receives a handler object via
|
||||
the `hdlr` parameter. This object is of type `RcpModuleHdlr` and vaguely
|
||||
comparable to the `app` (`PysimApp`) object found in `pySim-shell.py`.
|
||||
|
||||
The handler object provides the command method with the resources it needs to
|
||||
perform the card procedure.
|
||||
|
||||
rs, card, lchan
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
The Runtime State (`rs`), the Card (`card`) and the Lchan (`lchan`) Object
|
||||
have the same objectives asn in `pySim-shell.py`. Those objects work and are
|
||||
used the same way as they would in `pySim-shell.py`. It is assumed that the
|
||||
API user is already familiar with those objects.
|
||||
|
||||
cmd_args
|
||||
^^^^^^^^
|
||||
|
||||
The command arguments (`cmd_args`) contains the command line arguments as they
|
||||
were passed by the card holder on the `RCP Client` commandline in the form of
|
||||
a `Namespace` object.
|
||||
|
||||
Even though the command arguments are syntax-checked against the `args`
|
||||
description given in `cmd_descr`, caution is required to avoid security
|
||||
problems arising from malicious input.
|
||||
|
||||
keys_uicc and keys_euicc
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In case key material was requested via the `get_keys` in `cmd_descr`,
|
||||
`keys_uicc` and `keys_euicc` will contain those keys in the form of a dict. The
|
||||
dict key is the is the `CardKeyProvider` column name and the related dict value
|
||||
is the key material in its decrypted form.
|
||||
|
||||
When accessing `keys_uicc` and `keys_euicc`, extra care should be taken. It may
|
||||
make sense to delete/overwrite those dictionaries as soon as the keys were used
|
||||
for the intended purpose. However, due to python's internal memory management
|
||||
key material may remain longer in the system memory as expected.
|
||||
|
||||
print
|
||||
^^^^^
|
||||
|
||||
The `hdlr` object also provides a `print` method. This method accepts a string
|
||||
as the only parameter and can be used to display custom messages in the log
|
||||
output of the `RCP Client`. The method can be used to inform the card holder
|
||||
about the progress of a procedure or to print error messages in case a
|
||||
procedure fails.
|
||||
@@ -170,35 +170,6 @@ ensures that a message can only be sent once.
|
||||
.. note:: The replay-protection-counter is implemented as a 5 byte integer value (see also ETSI TS 102 225, Table 3).
|
||||
When the counter has reached its maximum, it will not overflow nor can it be reset.
|
||||
|
||||
Expanded remote application data format
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
`smpp-ota-tool` uses the TS 102 226 section 5.1 compact remote application data format by default. This
|
||||
format concatenates C-APDUs into one command string and only the result of the LAST executed command is reported back.
|
||||
Retrieving the response data therefore requires a GET RESPONSE C-APDU, and only a single GET RESPONSE command may occur per script.
|
||||
|
||||
The TS 102 226 section 5.2 expanded remote application data format removes these limitations: Each C-APDU is
|
||||
wrapped in its own C-APDU TLV inside a Command Scripting template, and the response is a Response Scripting template that contains one R-APDU TLV with the full response data and status word per executed command. To use it, pass
|
||||
``--format expanded``; every ``--apdu`` argument then becomes its own C-APDU TLV.
|
||||
|
||||
.. note:: The expanded format does not use GET RESPONSE. To retrieve response data from a case 2 or case 4
|
||||
command, include an ``Le`` field in the C-APDU. i.e. ``Le='00'`` instructs the card to return all available
|
||||
response data in the R-APDU, with no 256-byte limit (TS 102 226, section 5.2.1.1). Without the ``Le``
|
||||
field no response data is returned, except a status word for the last command!.
|
||||
|
||||
For example, a GP GET STATUS of all applications (``80F24002024F00``) returns a registry that can be much
|
||||
larger than 256 bytes. In the compact format the card would only answer with ``61xx`` procedure bytes. In the expanded
|
||||
format, appending ``Le='00'`` (i.e. ``80F24002024F0000``) makes the card return the whole registry in one exchange:
|
||||
|
||||
::
|
||||
|
||||
$ PYTHONPATH=./ ./contrib/smpp-ota-tool.py --kic <KIC> --kid <KID> --kid-idx 1 --kic-idx 1 \
|
||||
--algo-crypt triple_des_cbc2 --algo-auth triple_des_cbc2 --tar 000000 --cntr-req no_counter \
|
||||
--format expanded --apdu 80F24002024F0000
|
||||
|
||||
The response data (a concatenation of GlobalPlatform registry TLVs) can then be decoded with
|
||||
``pySim.global_platform.GpRegistryRelatedData.from_tlv()``.
|
||||
|
||||
smpp-ota-tool syntax
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
+3
-54
@@ -136,52 +136,6 @@ from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subjec
|
||||
import logging # noqa: E402
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _disable_twisted_alpn_if_incompatible():
|
||||
"""Twisted <-> pyOpenSSL TLS compatibility guard applied at import.
|
||||
|
||||
Twisted TLSMemoryBIOFactory applies ALPN by setting the 'select' callback
|
||||
on the SSL Context after it has already created a Connection from that
|
||||
Context (_createConnection -> _applyProtocolNegotiation).
|
||||
pyOpenSSL >= 25.0.0 makes a Context immutable once it has been used and
|
||||
raises, which aborts every inbound TLS handshake, client sees unexpected-EOF
|
||||
/ decode_error that looks like a cert/cipher problem but is not.
|
||||
pyOpenSSL < 25 does not import against recent cryptography, so downgrading
|
||||
it is not a fix.
|
||||
|
||||
This server only speaks HTTP/1.1 anyway, so ALPN negotiation is not
|
||||
needed.
|
||||
"""
|
||||
def _major(v):
|
||||
import re
|
||||
m = re.match(r'\d+', (v or '').strip())
|
||||
return int(m.group()) if m else 0
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
except Exception:
|
||||
return # no pyOpenSSL ???
|
||||
pyossl_ver = getattr(OpenSSL, '__version__', '0')
|
||||
if _major(pyossl_ver) < 25:
|
||||
return # pre-25 pyOpenSSL allows mutating a used Context
|
||||
|
||||
try:
|
||||
import twisted
|
||||
from twisted.protocols import tls
|
||||
except Exception:
|
||||
return
|
||||
factory = getattr(tls, 'TLSMemoryBIOFactory', None)
|
||||
if factory is None or not hasattr(factory, '_applyProtocolNegotiation'):
|
||||
return # Twisted already fixed
|
||||
|
||||
factory._applyProtocolNegotiation = lambda self, connection: None
|
||||
logger.warning("Disabled Twisted ALPN negotiation: Twisted %s + "
|
||||
"pyOpenSSL %s are incompatible for it",
|
||||
getattr(twisted, '__version__', '?'), pyossl_ver)
|
||||
|
||||
|
||||
_disable_twisted_alpn_if_incompatible()
|
||||
|
||||
# HACK: make this configurable
|
||||
DATA_DIR = './smdpp-data'
|
||||
HOSTNAME = 'testsmdpplus1.example.com' # must match certificates!
|
||||
@@ -525,7 +479,7 @@ class SmDppHttpServer:
|
||||
"""See ES9+ InitiateAuthentication SGP.22 Section 5.6.1"""
|
||||
# Verify that the received address matches its own SM-DP+ address, where the comparison SHALL be
|
||||
# case-insensitive. Otherwise, the SM-DP+ SHALL return a status code "SM-DP+ Address - Refused".
|
||||
if content['smdpAddress'].lower() != self.server_hostname.lower():
|
||||
if content['smdpAddress'] != self.server_hostname:
|
||||
raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address')
|
||||
|
||||
euiccChallenge = b64decode(content['euiccChallenge'])
|
||||
@@ -916,17 +870,12 @@ def main(argv):
|
||||
action='store_true', default=False)
|
||||
parser.add_argument("-m", "--in-memory", help="Use ephermal in-memory session storage (for concurrent runs)",
|
||||
action='store_true', default=False)
|
||||
parser.add_argument("--smdp-address", default=HOSTNAME,
|
||||
help="ES9+ SM-DP+ address advertised, defaults to \"%(default)s\". "
|
||||
"Include the TLS port (e.g. %(default)s:8443) when binding a port other "
|
||||
"than 443, so it matches the address the LPA connects to. "
|
||||
"The TLS certificate identity is unaffected.")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING)
|
||||
|
||||
common_cert_path = os.path.join(DATA_DIR, args.certdir)
|
||||
hs = SmDppHttpServer(server_hostname=args.smdp_address, ci_certs_path=os.path.join(common_cert_path, 'CertificateIssuer'), common_cert_path=common_cert_path, use_brainpool=args.brainpool)
|
||||
hs = SmDppHttpServer(server_hostname=HOSTNAME, ci_certs_path=os.path.join(common_cert_path, 'CertificateIssuer'), common_cert_path=common_cert_path, use_brainpool=args.brainpool)
|
||||
if(args.nossl):
|
||||
hs.app.run(args.host, args.port)
|
||||
else:
|
||||
@@ -955,7 +904,7 @@ def main(argv):
|
||||
with open(cert_pempath, 'wb') as pem_file:
|
||||
pem_file.write(pem_cert)
|
||||
|
||||
SERVER_STRING = f'ssl:{args.port}:privateKey={cert_skpath}:certKey={cert_pempath}:dhParameters={dhparam_path}:interface={args.host}'
|
||||
SERVER_STRING = f'ssl:{args.port}:privateKey={cert_skpath}:certKey={cert_pempath}:dhParameters={dhparam_path}'
|
||||
print(SERVER_STRING)
|
||||
|
||||
hs.app.run(host=HOSTNAME, port=args.port, endpoint_description=SERVER_STRING)
|
||||
|
||||
+1
-1
@@ -816,7 +816,7 @@ if __name__ == '__main__':
|
||||
print("")
|
||||
print("Card programming failed with an exception:")
|
||||
print("---------------------8<---------------------")
|
||||
print(traceback.format_exc().rstrip())
|
||||
traceback.print_exc()
|
||||
print("---------------------8<---------------------")
|
||||
print("")
|
||||
rc = -1
|
||||
|
||||
+19
-21
@@ -101,7 +101,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.numeric_path = False
|
||||
self.conserve_write = True
|
||||
self.json_pretty_print = True
|
||||
self.apdu_trace = getattr(sl, 'apdu_tracer', None) is not None
|
||||
self.apdu_trace = False
|
||||
self.apdu_strict = False
|
||||
|
||||
self.add_settable(cmd2.Settable('numeric_path', bool,
|
||||
@@ -210,10 +210,8 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
def __init__(self, cmd2_app):
|
||||
self.cmd2 = cmd2_app
|
||||
|
||||
def trace_command(self, cmd):
|
||||
self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
|
||||
|
||||
def trace_response(self, cmd, sw, resp):
|
||||
self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
|
||||
self.cmd2.poutput("<- %s: %s" % (sw, resp))
|
||||
|
||||
def update_prompt(self):
|
||||
@@ -351,7 +349,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.poutput("")
|
||||
self.poutput("Card initialization (%s) failed with an exception:" % str(self.sl))
|
||||
self.poutput("---------------------8<---------------------")
|
||||
self.poutput(traceback.format_exc().rstrip())
|
||||
traceback.print_exc()
|
||||
self.poutput("---------------------8<---------------------")
|
||||
self.poutput("")
|
||||
return -1
|
||||
@@ -465,7 +463,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.poutput("")
|
||||
self.poutput("Card handling (%s) failed with an exception:" % str(self.sl))
|
||||
self.poutput("---------------------8<---------------------")
|
||||
self.poutput(traceback.format_exc().rstrip())
|
||||
traceback.print_exc()
|
||||
self.poutput("---------------------8<---------------------")
|
||||
self.poutput("")
|
||||
fail_count = fail_count + 1
|
||||
@@ -1131,13 +1129,10 @@ global_group.add_argument("--verbose", help="Enable verbose logging",
|
||||
action='store_true', default=False)
|
||||
|
||||
adm_group = global_group.add_mutually_exclusive_group()
|
||||
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM', dest='pin_adm', default=None,
|
||||
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
|
||||
help='ADM PIN used for provisioning (overwrites default)')
|
||||
adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM_HEX', dest='pin_adm_hex', default=None,
|
||||
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)')
|
||||
global_group.add_argument('--pin-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')
|
||||
|
||||
option_parser.add_argument('-e', '--execute-command', action='append', default=[],
|
||||
help='A pySim-shell command that will be executed at startup')
|
||||
@@ -1176,7 +1171,7 @@ if __name__ == '__main__':
|
||||
startup_errors = True
|
||||
print("Card initialization (%s) failed with an exception:" % str(sl))
|
||||
print("---------------------8<---------------------")
|
||||
print(traceback.format_exc().rstrip())
|
||||
traceback.print_exc()
|
||||
print("---------------------8<---------------------")
|
||||
if not opts.noprompt:
|
||||
print("(you may still try to recover from this manually by using the 'equip' command.)")
|
||||
@@ -1187,15 +1182,18 @@ if __name__ == '__main__':
|
||||
|
||||
# If the user supplies an ADM PIN at via commandline args authenticate
|
||||
# immediately so that the user does not have to use the shell commands
|
||||
pin_adm_type = ""
|
||||
if opts.pin_adm_type:
|
||||
pin_adm_type = "--adm-type %s" % opts.pin_adm_type
|
||||
if opts.pin_adm:
|
||||
app.onecmd_plus_hooks("verify_adm %s %s" %
|
||||
(opts.pin_adm, pin_adm_type), add_to_history = False)
|
||||
elif opts.pin_adm_hex:
|
||||
app.onecmd_plus_hooks("verify_adm %s --pin-is-hex %s" %
|
||||
(opts.pin_adm_hex, pin_adm_type), add_to_history = False)
|
||||
pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
|
||||
if pin_adm:
|
||||
if not card:
|
||||
print("Card error, cannot do ADM verification with supplied ADM pin now.")
|
||||
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:
|
||||
|
||||
+227
-33
@@ -30,13 +30,10 @@
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import colorlog
|
||||
|
||||
from twisted.protocols import basic
|
||||
from twisted.internet import defer, endpoints, reactor, task
|
||||
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
|
||||
@@ -50,16 +47,13 @@ from smpp.pdu import pdu_types, operations, pdu_encoding
|
||||
|
||||
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
|
||||
|
||||
from pySim.bip import Proact, terminal_profile
|
||||
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 sms_pp_download_envelope
|
||||
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.cat import EventList, EventDownload, Result
|
||||
from pySim.utils import b2h, h2b
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -77,6 +71,224 @@ class MyApduTracer(ApduTracer):
|
||||
print("-> %s %s" % (cmd[:10], cmd[10:]))
|
||||
print("<- %s: %s" % (sw, resp))
|
||||
|
||||
class TcpProtocol(protocol.Protocol):
|
||||
def dataReceived(self, data):
|
||||
pass
|
||||
|
||||
def connectionLost(self, reason):
|
||||
pass
|
||||
|
||||
|
||||
def tcp_connected_callback(p: protocol.Protocol):
|
||||
"""called by twisted TCP client."""
|
||||
logger.error("%s: connected!" % p)
|
||||
|
||||
class ProactChannel:
|
||||
"""Representation of a single protective channel."""
|
||||
def __init__(self, channels: 'ProactChannels', chan_nr: int):
|
||||
self.channels = channels
|
||||
self.chan_nr = chan_nr
|
||||
self.ep = None
|
||||
|
||||
def close(self):
|
||||
"""Close the channel."""
|
||||
if self.ep:
|
||||
self.ep.disconnect()
|
||||
self.channels.channel_delete(self.chan_nr)
|
||||
|
||||
class ProactChannels:
|
||||
"""Wrapper class for maintaining state of proactive channels."""
|
||||
def __init__(self):
|
||||
self.channels = {}
|
||||
|
||||
def channel_create(self) -> ProactChannel:
|
||||
"""Create a new proactive channel, allocating its integer number."""
|
||||
for i in range(1, 9):
|
||||
if not i in self.channels:
|
||||
self.channels[i] = ProactChannel(self, i)
|
||||
return self.channels[i]
|
||||
raise ValueError('Cannot allocate another channel: All channels active')
|
||||
|
||||
def channel_delete(self, chan_nr: int):
|
||||
del self.channels[chan_nr]
|
||||
|
||||
class Proact(ProactiveHandler):
|
||||
#def __init__(self, smpp_factory):
|
||||
# self.smpp_factory = smpp_factory
|
||||
def __init__(self):
|
||||
self.channels = ProactChannels()
|
||||
|
||||
@staticmethod
|
||||
def _find_first_element_of_type(instlist, cls):
|
||||
for i in instlist:
|
||||
if isinstance(i, cls):
|
||||
return i
|
||||
return None
|
||||
|
||||
"""Call-back which the pySim transport core calls whenever it receives a
|
||||
proactive command from the SIM."""
|
||||
def handle_SendShortMessage(self, pcmd: ProactiveCommand):
|
||||
# {'smspp_download': [{'device_identities': {'source_dev_id': 'network',
|
||||
# 'dest_dev_id': 'uicc'}},
|
||||
# {'address': {'ton_npi': {'ext': True,
|
||||
# 'type_of_number': 'international',
|
||||
# 'numbering_plan_id': 'isdn_e164'},
|
||||
# 'call_number': '79'}},
|
||||
# {'sms_tpdu': {'tpdu': '40048111227ff6407070611535004d02700000481516011212000001fe4c0943aea42e45021c078ae06c66afc09303608874b72f58bacadb0dcf665c29349c799fbb522e61709c9baf1890015e8e8e196e36153106c8b92f95153774'}}
|
||||
# ]}
|
||||
"""Card requests sending a SMS. We need to pass it on to the ESME via SMPP."""
|
||||
logger.info("SendShortMessage")
|
||||
logger.info(pcmd)
|
||||
# Relevant parts in pcmd: Address, SMS_TPDU
|
||||
addr_ie = Proact._find_first_element_of_type(pcmd.children, Address)
|
||||
sms_tpdu_ie = Proact._find_first_element_of_type(pcmd.children, SMS_TPDU)
|
||||
raw_tpdu = sms_tpdu_ie.decoded['tpdu']
|
||||
submit = SMS_SUBMIT.from_bytes(raw_tpdu)
|
||||
submit.tp_da = AddressField(addr_ie.decoded['call_number'], addr_ie.decoded['ton_npi']['type_of_number'],
|
||||
addr_ie.decoded['ton_npi']['numbering_plan_id'])
|
||||
logger.info(submit)
|
||||
self.send_sms_via_smpp(submit)
|
||||
|
||||
def handle_OpenChannel(self, pcmd: ProactiveCommand):
|
||||
"""Card requests opening a new channel via a UDP/TCP socket."""
|
||||
# {'open_channel': [{'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'open_channel',
|
||||
# 'command_qualifier': 3}},
|
||||
# {'device_identities': {'source_dev_id': 'uicc',
|
||||
# 'dest_dev_id': 'terminal'}},
|
||||
# {'bearer_description': {'bearer_type': 'default',
|
||||
# 'bearer_parameters': ''}},
|
||||
# {'buffer_size': 1024},
|
||||
# {'uicc_transport_level': {'protocol_type': 'tcp_uicc_client_remote',
|
||||
# 'port_number': 32768}},
|
||||
# {'other_address': {'type_of_address': 'ipv4',
|
||||
# 'address': '01020304'}}
|
||||
# ]}
|
||||
logger.info("OpenChannel")
|
||||
logger.info(pcmd)
|
||||
transp_lvl_ie = Proact._find_first_element_of_type(pcmd.children, UiccTransportLevel)
|
||||
other_addr_ie = Proact._find_first_element_of_type(pcmd.children, OtherAddress)
|
||||
bearer_desc_ie = Proact._find_first_element_of_type(pcmd.children, BearerDescription)
|
||||
buffer_size_ie = Proact._find_first_element_of_type(pcmd.children, BufferSize)
|
||||
if transp_lvl_ie.decoded['protocol_type'] != 'tcp_uicc_client_remote':
|
||||
raise ValueError('Unsupported protocol_type')
|
||||
if other_addr_ie.decoded.get('type_of_address', None) != 'ipv4':
|
||||
raise ValueError('Unsupported type_of_address')
|
||||
ipv4_bytes = h2b(other_addr_ie.decoded['address'])
|
||||
ipv4_str = '%u.%u.%u.%u' % (ipv4_bytes[0], ipv4_bytes[1], ipv4_bytes[2], ipv4_bytes[3])
|
||||
port_nr = transp_lvl_ie.decoded['port_number']
|
||||
print("%s:%u" % (ipv4_str, port_nr))
|
||||
channel = self.channels.channel_create()
|
||||
channel.ep = endpoints.TCP4ClientEndpoint(reactor, ipv4_str, port_nr)
|
||||
channel.prot = TcpProtocol()
|
||||
d = endpoints.connectProtocol(channel.ep, channel.prot)
|
||||
# FIXME: why is this never called despite the client showing the inbound connection?
|
||||
d.addCallback(tcp_connected_callback)
|
||||
|
||||
# Terminal Response example: [
|
||||
# {'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'open_channel',
|
||||
# 'command_qualifier': 3}},
|
||||
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
||||
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
||||
# {'channel_status': '8100'},
|
||||
# {'bearer_description': {'bearer_type': 'default', 'bearer_parameters': ''}},
|
||||
# {'buffer_size': 1024}
|
||||
# ]
|
||||
return self.prepare_response(pcmd) + [ChannelStatus(decoded='8100'), bearer_desc_ie, buffer_size_ie]
|
||||
|
||||
def handle_CloseChannel(self, pcmd: ProactiveCommand):
|
||||
"""Close a channel."""
|
||||
logger.info("CloseChannel")
|
||||
logger.info(pcmd)
|
||||
|
||||
def handle_ReceiveData(self, pcmd: ProactiveCommand):
|
||||
"""Receive/read data from the socket."""
|
||||
# {'receive_data': [{'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'receive_data',
|
||||
# 'command_qualifier': 0}},
|
||||
# {'device_identities': {'source_dev_id': 'uicc',
|
||||
# 'dest_dev_id': 'channel_1'}},
|
||||
# {'channel_data_length': 9}
|
||||
# ]}
|
||||
logger.info("ReceiveData")
|
||||
logger.info(pcmd)
|
||||
# Terminal Response example: [
|
||||
# {'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'receive_data',
|
||||
# 'command_qualifier': 0}},
|
||||
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
||||
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
||||
# {'channel_data': '16030100040e000000'},
|
||||
# {'channel_data_length': 0}
|
||||
# ]
|
||||
return self.prepare_response(pcmd) + []
|
||||
|
||||
def handle_SendData(self, pcmd: ProactiveCommand):
|
||||
"""Send/write data received from the SIM to the socket."""
|
||||
# {'send_data': [{'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'send_data',
|
||||
# 'command_qualifier': 1}},
|
||||
# {'device_identities': {'source_dev_id': 'uicc',
|
||||
# 'dest_dev_id': 'channel_1'}},
|
||||
# {'channel_data': '160301003c010000380303d0f45e12b52ce5bb522750dd037738195334c87a46a847fe2b6886cada9ea6bf00000a00ae008c008b00b0002c010000050001000101'}
|
||||
# ]}
|
||||
logger.info("SendData")
|
||||
logger.info(pcmd)
|
||||
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
|
||||
chan_data_ie = Proact._find_first_element_of_type(pcmd.children, ChannelData)
|
||||
chan_str = dev_id_ie.decoded['dest_dev_id']
|
||||
chan_nr = 1 # FIXME
|
||||
chan = self.channels.channels.get(chan_nr, None)
|
||||
# FIXME chan.prot.transport.write(h2b(chan_data_ie.decoded))
|
||||
# Terminal Response example: [
|
||||
# {'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'send_data',
|
||||
# 'command_qualifier': 1}},
|
||||
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
||||
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
||||
# {'channel_data_length': 255}
|
||||
# ]
|
||||
return self.prepare_response(pcmd) + [ChannelDataLength(decoded=255)]
|
||||
|
||||
def handle_SetUpEventList(self, pcmd: ProactiveCommand):
|
||||
# {'set_up_event_list': [{'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'set_up_event_list',
|
||||
# 'command_qualifier': 0}},
|
||||
# {'device_identities': {'source_dev_id': 'uicc',
|
||||
# 'dest_dev_id': 'terminal'}},
|
||||
# {'event_list': ['data_available', 'channel_status']}
|
||||
# ]}
|
||||
logger.info("SetUpEventList")
|
||||
logger.info(pcmd)
|
||||
# Terminal Response example: [
|
||||
# {'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'set_up_event_list',
|
||||
# 'command_qualifier': 0}},
|
||||
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
||||
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}}
|
||||
# ]
|
||||
return self.prepare_response(pcmd)
|
||||
|
||||
def getChannelStatus(self, pcmd: ProactiveCommand):
|
||||
logger.info("GetChannelStatus")
|
||||
logger.info(pcmd)
|
||||
return self.prepare_response(pcmd) + []
|
||||
|
||||
def send_sms_via_smpp(self, submit: SMS_SUBMIT):
|
||||
# while in a normal network the phone/ME would *submit* a message to the SMSC,
|
||||
# we are actually emulating the SMSC itself, so we must *deliver* the message
|
||||
# to the ESME
|
||||
deliver = SMS_DELIVER.from_submit(submit)
|
||||
deliver_smpp = deliver.to_smpp()
|
||||
|
||||
hackish_global_smpp.sendDataRequest(deliver_smpp)
|
||||
# # obtain the connection/binding of system_id to be used for delivering MO-SMS to the ESME
|
||||
# connection = smpp_server.getBoundConnections[system_id].getNextBindingForDelivery()
|
||||
# connection.sendDataRequest(deliver_smpp)
|
||||
|
||||
|
||||
|
||||
def dcs_is_8bit(dcs):
|
||||
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
||||
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
|
||||
@@ -111,11 +323,6 @@ class MyServer:
|
||||
smppEndpoint = endpoints.TCP6ServerEndpoint(reactor, tcp_port, interface=bind_ip)
|
||||
smppEndpoint.listen(self.factory)
|
||||
self.tp = self.scc = self.card = None
|
||||
# Serialise card/APDU access.
|
||||
# - SMPP handler drives the card from reactor thread
|
||||
# - BIP relay data-available path drives it from socket reader thread.
|
||||
# The transport is not re-entrant, both must take this lock.
|
||||
self._card_lock = threading.Lock()
|
||||
|
||||
def connect_to_card(self, tp: LinkBase):
|
||||
self.tp = tp
|
||||
@@ -126,22 +333,8 @@ class MyServer:
|
||||
self.scc.sel_ctrl = "0004"
|
||||
self.card.read_aids()
|
||||
self.card.select_adf_by_aid(adf='usim')
|
||||
self.scc.terminal_profile(b2h(terminal_profile()))
|
||||
# Connect the BIP relay inbound path to the card.
|
||||
# relay socket receives data -> ME initiated ENVELOPE EVENT DOWNLOA
|
||||
# -> triggers RECEIVE DATA proactive session.
|
||||
# FIXME this cross-thread push to the card is exercised only with real hardware
|
||||
# the card free tests cover socket relay + envelope construction, not delivery.
|
||||
handler = getattr(tp, 'proactive_handler', None)
|
||||
if isinstance(handler, Proact):
|
||||
handler.data_available_sink = self._deliver_data_available
|
||||
|
||||
def _deliver_data_available(self, envelope_hex: str):
|
||||
"""push ME initiated ENVELOPE EVENT DOWNLOAD to the card"""
|
||||
with self._card_lock:
|
||||
logger.info("ENVELOPE(Data available): %s" % envelope_hex)
|
||||
(data, sw) = self.scc.envelope(envelope_hex)
|
||||
logger.info("SW %s: %s" % (sw, data))
|
||||
# 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."""
|
||||
@@ -169,12 +362,14 @@ class MyServer:
|
||||
tpdu = SMS_DELIVER.from_smpp_submit(pdu)
|
||||
logger.info(tpdu)
|
||||
# 2) wrap into the CAT ENVELOPE for SMS-PP-Download
|
||||
sms_dl = sms_pp_download_envelope(tpdu)
|
||||
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)
|
||||
with self._card_lock:
|
||||
(data, sw) = self.scc.envelope(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'
|
||||
@@ -221,8 +416,7 @@ if __name__ == '__main__':
|
||||
|
||||
opts = option_parser.parse_args()
|
||||
|
||||
tp = init_reader(opts, proactive_handler = Proact(
|
||||
sms_sink=lambda pdu: hackish_global_smpp.sendDataRequest(pdu)))
|
||||
tp = init_reader(opts, proactive_handler = Proact())
|
||||
if tp is None:
|
||||
exit(1)
|
||||
tp.connect()
|
||||
|
||||
+3
-4
@@ -30,11 +30,10 @@ from pySim.log import PySimLogger
|
||||
|
||||
log = PySimLogger.get(__name__)
|
||||
|
||||
# we need to import these modules so that the SysmocomSJA2 / SysmocomSJS1
|
||||
# sub-classes of CardModel are created, which will add the ATR-based matching
|
||||
# and calling of their add_files. See CardModel.apply_matching_models
|
||||
# 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_sjs1
|
||||
|
||||
# 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
|
||||
|
||||
-627
@@ -1,627 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Bearer Independent Protocol relay"""
|
||||
|
||||
#
|
||||
# (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/>.
|
||||
|
||||
# A ProactiveHandler with TCP sockets that backs the BIP channels,
|
||||
# so a card can run its own IP session (SCP81/HTTPS, CAT_TP, ...)
|
||||
#
|
||||
# Currently used by pySim-smpp2sim.py which connects the SMS path to its SMPP server.
|
||||
# Other drivers can pass their own sinks:
|
||||
#
|
||||
# handler = Proact(data_available_sink=..., sms_sink=...)
|
||||
# tp = init_reader(opts, proactive_handler=handler)
|
||||
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from osmocom.utils import b2h, h2b
|
||||
|
||||
from pySim.transport import ProactiveHandler
|
||||
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
|
||||
from pySim.cat import (ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload,
|
||||
BearerDescription, DeviceIdentities, Address, OtherAddress,
|
||||
UiccTransportLevel, BufferSize, ChannelStatus, ChannelData,
|
||||
ChannelDataLength, EventList, EventDownload, Result,
|
||||
CommandDetails, LocationInformation)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# PROVIDE LOCAL INFORMATION location, GERAN TS 31.111 8.19.1
|
||||
# - 3 byte PLMN of TS 24.008 10.5.1.3 -> 262-01
|
||||
# - 2 byte LAC and a 2 byte cid.
|
||||
DEFAULT_LOCATION = h2b('62f21000010001')
|
||||
|
||||
|
||||
def terminal_profile(num_channels: int = 7) -> bytes:
|
||||
"""TERMINAL PROFILE for what we implement, TS 102 223 5.2 and annex T.
|
||||
|
||||
Annex T table T.1 lists what a Connected Entity, a CAT client that is not the modem
|
||||
which is pretty much what we are, may announce, and its inverse is what only a modem may announce.
|
||||
"""
|
||||
if not 0 <= num_channels <= ProactChannels.MAX_CHANNELS:
|
||||
raise ValueError('num_channels must be 0..%u' % ProactChannels.MAX_CHANNELS)
|
||||
profile = bytearray(32)
|
||||
# 1 (Download): b1 profile download, b2+b5 SMS-PP data download. Both of the latter, per the
|
||||
# note in TS 31.111 5.2: "several bits may need to be set to 1 for the support of the same
|
||||
# facility ... because of backward compatibility with SAT". The relay is OTA over SMS-PP.
|
||||
profile[0] = 0x01 | 0x02 | 0x10
|
||||
profile[1] = 0x01 # 2 (Other): b1 command result
|
||||
profile[2] = 0x80 # 3: b8 REFRESH (empty result is a valid answer, 6.4.7)
|
||||
profile[3] = 0x02 # 4: b2 SEND SHORT MESSAGE (the OTA response path)
|
||||
profile[4] = 0x01 # 5: b1 SET UP EVENT LIST
|
||||
profile[5] = 0x04 | 0x08 # 6: b3 Event Data available, b4 Event Channel status
|
||||
# 12 (class "e"): b1..b5 OPEN CHANNEL, CLOSE CHANNEL, RECEIVE DATA, SEND DATA, GET CHANNEL
|
||||
# STATUS.
|
||||
profile[11] = 0x1f
|
||||
# 13 (class "e" supported bearers): b2 GPRS, and b6..b8 the number of channels.
|
||||
profile[12] = 0x02 | (num_channels << 5)
|
||||
profile[13] = 0x40 | 0x20 # 14: b6 no display capability, b7 no keypad available
|
||||
profile[16] = 0x01 # 15: b1 TCP, UICC in client mode, remote connection
|
||||
return bytes(profile)
|
||||
|
||||
|
||||
class ProactChannel:
|
||||
"""One BIP channel, TS 102 223 class "e", backed by a blocking TCP socket.
|
||||
|
||||
Created by ProactChannels.channel_create(). A reader thread fills the Rx buffer from the
|
||||
socket, the Proact handlers drain it (RECEIVE DATA) and write to it (SEND DATA). Payload
|
||||
is opaque, TLS or CAT_TP run on the card.
|
||||
|
||||
Args:
|
||||
channels: the owning ProactChannels, notified of data arrival and of close()
|
||||
chan_nr: channel number 1..7 as used in the Device identities
|
||||
"""
|
||||
# Why blocking sockets and not Twisted endpoints, considering we have twisted?
|
||||
# The proactive-command loop lives in a blocking while-loop,
|
||||
# "pySim.transport.LinkBase.send_apdu_checksw" that runs on the Twisted reactor thread.
|
||||
# A Twisted async TCP client only makes any progress when the reactor uhh... reacts, but
|
||||
# the reactor is stuck in that loop for the whole proactive session -> the
|
||||
# connectProtocol() Deferred never fires while we are handling OPEN/SEND/RECEIVE CHANNEL.
|
||||
# Plain blocking sockets just work: connect() in handle_OpenChannel, send() in
|
||||
# handle_SendData, recv() feeding a buffer for handle_ReceiveData. No need to make it
|
||||
# harder than it has to be to handle the "massive" T0 bandwidth..
|
||||
# how much we try to read off the socket per recv()
|
||||
RECV_CHUNK = 4096
|
||||
|
||||
def __init__(self, channels: 'ProactChannels', chan_nr: int):
|
||||
self.channels = channels
|
||||
self.chan_nr = chan_nr
|
||||
self.sock = None
|
||||
# TS 102 223 says the terminal keeps an Rx buffer per channel; RECEIVE
|
||||
# DATA drains it, and it is filled asynchronously as the peer sends.
|
||||
self.rx_buf = bytearray()
|
||||
self._rx_lock = threading.Lock()
|
||||
self._reader = None
|
||||
self._closing = False
|
||||
self.peer_closed = False
|
||||
|
||||
def connect(self, host: str, port: int, timeout: float = 10.0):
|
||||
"""Open the blocking TCP socket and start the background Rx reader."""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
s.settimeout(timeout)
|
||||
s.connect((host, port))
|
||||
# Back to blocking mode for the reader thread.
|
||||
# CLOSE CHANNEL unblocks the pending recv() via shutdown().
|
||||
s.settimeout(None)
|
||||
except OSError:
|
||||
s.close()
|
||||
raise
|
||||
self.sock = s
|
||||
self._reader = threading.Thread(target=self._rx_loop,
|
||||
name='bip-rx-%d' % self.chan_nr, daemon=True)
|
||||
self._reader.start()
|
||||
|
||||
def _rx_loop(self):
|
||||
"""Continuously read from the socket into rx_buf, like a real ME.
|
||||
|
||||
TS 102 223 7.5.10.1 says the event is raised 'only if the targeted channel buffer is
|
||||
empty when new data arrives in it', so the data available hook fires on the
|
||||
empty->non-empty transition only. That is enough: every RECEIVE DATA response tells
|
||||
the card how many bytes remain, so it keeps fetching until the buffer is empty, and
|
||||
the next event restarts it when more data arrives."""
|
||||
while not self._closing:
|
||||
try:
|
||||
data = self.sock.recv(self.RECV_CHUNK)
|
||||
except (OSError, ValueError):
|
||||
break
|
||||
if not data:
|
||||
self.peer_closed = True
|
||||
break
|
||||
with self._rx_lock:
|
||||
was_empty = len(self.rx_buf) == 0
|
||||
self.rx_buf.extend(data)
|
||||
if was_empty and not self._closing:
|
||||
self.channels.notify_data_available(self)
|
||||
|
||||
def send(self, data: bytes):
|
||||
"""Tx, write bytes to the socket == SEND DATA"""
|
||||
self.sock.sendall(data)
|
||||
|
||||
def available_rx(self) -> int:
|
||||
"""Number of bytes waiting in the Rx buffer, what RECEIVE DATA can return right now."""
|
||||
with self._rx_lock:
|
||||
return len(self.rx_buf)
|
||||
|
||||
def take_rx(self, n: int):
|
||||
"""Take up to n bytes out of the Rx buffer. Returns (bytes, bytes still remaining)."""
|
||||
with self._rx_lock:
|
||||
chunk = bytes(self.rx_buf[:n])
|
||||
del self.rx_buf[:n]
|
||||
remaining = len(self.rx_buf)
|
||||
return chunk, remaining
|
||||
|
||||
def wait_rx(self, timeout: float) -> int:
|
||||
"""wait up to timeout seconds until the rxbuf has data
|
||||
returns the number of bytes available
|
||||
Cards have a "data available" event, card free callers use
|
||||
this to wait for the echoed bytes."""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
avail = self.available_rx()
|
||||
if avail or self.peer_closed:
|
||||
return avail
|
||||
time.sleep(0.005)
|
||||
return self.available_rx()
|
||||
|
||||
def close(self):
|
||||
"""Close channel: stop reader, close socket, drop bookkeeping."""
|
||||
self._closing = True
|
||||
if self.sock is not None:
|
||||
try:
|
||||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
self.sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
# CLOSE CHANNEL synchronously handled inside the rx reader thread
|
||||
# (data-available -> ENVELOPE -> FETCH -> handle_CloseChannel -> close),
|
||||
# so close() can be called on the reader thread.
|
||||
# Joining self raises "cannot join current thread" so better skip i..
|
||||
# setting _closing + shutting down the socket already makes _rx_loop
|
||||
# return on the next iteration anyway.
|
||||
if self._reader is not None and self._reader is not threading.current_thread():
|
||||
self._reader.join(timeout=1.0)
|
||||
self.channels.channel_delete(self.chan_nr)
|
||||
|
||||
class ProactChannels:
|
||||
"""The open BIP channels of one terminal, keyed by channel number.
|
||||
|
||||
Args:
|
||||
on_data_available: callback(chan: ProactChannel), invoked from the channel's reader
|
||||
thread when data arrives in an empty Rx buffer. Proact turns it into an
|
||||
ENVELOPE EVENT DOWNLOAD (data available).
|
||||
"""
|
||||
|
||||
# TS 102 223 8.56 channel identifier in 3 bits as "1 to 7", 0 == no channel available
|
||||
# TERMINAL PROFILE has to agree with byte 13 , "number of channels supported by terminal"
|
||||
MAX_CHANNELS = 7
|
||||
|
||||
def __init__(self, on_data_available=None):
|
||||
self.channels = {}
|
||||
self._on_data_available = on_data_available
|
||||
|
||||
def channel_create(self) -> ProactChannel:
|
||||
"""Create a new proactive channel, allocating its integer number."""
|
||||
for i in range(1, self.MAX_CHANNELS + 1):
|
||||
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):
|
||||
"""Forget a channel, called by ProactChannel.close()."""
|
||||
self.channels.pop(chan_nr, None)
|
||||
|
||||
def notify_data_available(self, chan: ProactChannel):
|
||||
"""Run the on_data_available callback for chan, if one was given."""
|
||||
if self._on_data_available:
|
||||
self._on_data_available(chan)
|
||||
|
||||
class Proact(ProactiveHandler):
|
||||
"""ProactiveHandler that answers the BIP proactive commands with TCP sockets.
|
||||
|
||||
The transport calls the handle_* methods with the decoded proactive command and posts
|
||||
the returned IE list as TERMINAL RESPONSE.
|
||||
|
||||
Args:
|
||||
data_available_sink: callback(envelope_hex: str), called from a channel reader thread
|
||||
with an encoded ENVELOPE EVENT DOWNLOAD (data available). The caller forwards it
|
||||
to the card with the ENVELOPE command, the card then FETCHes RECEIVE DATA.
|
||||
None: the event is only logged (card free / test mode).
|
||||
sms_sink: callback(pdu), called with the SMPP deliver_sm of a SEND SHORT MESSAGE
|
||||
the card issued; pySim-smpp2sim.py hands it to its SMPP server.
|
||||
None: the SMS is logged and dropped.
|
||||
location: Location information returned in PROVIDE LOCAL INFORMATION (location).
|
||||
"""
|
||||
def __init__(self, data_available_sink=None, sms_sink=None, location: bytes = DEFAULT_LOCATION):
|
||||
self.data_available_sink = data_available_sink
|
||||
self.sms_sink = sms_sink
|
||||
self.location = location
|
||||
self.channels = ProactChannels(on_data_available=self._on_channel_data_available)
|
||||
|
||||
def handle_ProvideLocalInformation(self, pcmd: ProactiveCommand):
|
||||
"""only location
|
||||
|
||||
TS 102 223 6.8.7 says TERMINAL RESPONSE to PROVIDE LOCAL INFORMATION "shall"
|
||||
contain the data object the command qualifier (6.6.15) asked for. At least answer '00',
|
||||
location information, usually requested.
|
||||
|
||||
answering "terminal currently unable to process - no service", which is a handset
|
||||
out of coverage makes SJA5 believe it and postpones the entire session!
|
||||
it registers a location status event, starts a ten minute timer and waits for coverage."""
|
||||
cmd_det_ie = Proact._find_first_element_of_type(pcmd.children, CommandDetails)
|
||||
if cmd_det_ie is not None and cmd_det_ie.decoded['command_qualifier'] == 0x00:
|
||||
return self.prepare_response(pcmd) + [LocationInformation(decoded=self.location)]
|
||||
return self.prepare_response(pcmd)
|
||||
|
||||
def receive_fetch(self, pcmd: ProactiveCommand):
|
||||
"""Answer anything this handler has no specific handler for.
|
||||
|
||||
A card coming up will usually issue PROVIDE LOCAL INFORMATION,
|
||||
POLL INTERVAL or TIMER MANAGEMENT before it gets anywhere near a BIP channel,
|
||||
whatever the TERMINAL PROFILE announces.
|
||||
|
||||
Note that this is not the spec-correct answer. TS 102 223 6.8.7
|
||||
says a successful TERMINAL RESPONSE to PROVIDE LOCAL INFORMATION "shall" carry the
|
||||
requested Local information data object, and 6.8.13/6.8.14 says the same for TIMER
|
||||
MANAGEMENT, this returns empty results for all of them, which works with real cards.
|
||||
|
||||
Always "performed_successfully", never "command_beyond_terminal_capability" because
|
||||
answering that to PROVIDE LOCAL INFORMATION makes a card refuse to open the session.
|
||||
"""
|
||||
logger.info("no handler for %s, answering performed_successfully",
|
||||
type(pcmd.decoded).__name__)
|
||||
return self.prepare_response(pcmd, 'performed_successfully')
|
||||
|
||||
@staticmethod
|
||||
def _find_first_element_of_type(instlist, cls):
|
||||
for i in instlist:
|
||||
if isinstance(i, cls):
|
||||
return i
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _channel_nr_from_dev_ids(dev_id_ie: DeviceIdentities) -> int:
|
||||
"""Maps id like channel_1 -> channel number.
|
||||
TS 102 223 Section 8.7 says low nibble is channel number,
|
||||
channel-N = 0x21..0x27"""
|
||||
dest = dev_id_ie.decoded['dest_dev_id']
|
||||
return DeviceIdentities.DEV_IDS.inverse[dest] & 0x0f
|
||||
|
||||
def _channel_for(self, dev_id_ie: DeviceIdentities):
|
||||
"""Resolve the ProactChannel addressed by a command dev id, or None"""
|
||||
return self.channels.channels.get(self._channel_nr_from_dev_ids(dev_id_ie), None)
|
||||
|
||||
@staticmethod
|
||||
def _channel_status(chan_nr: int, established: bool = True) -> str:
|
||||
"""TS 102 223 Section 8.56 channel status value for the
|
||||
default/network bearer:
|
||||
- byte 3 low 3 bits = channel id
|
||||
- bit 8 = link established
|
||||
- byte 4 = 00 no further info"""
|
||||
b3 = (0x80 if established else 0x00) | (chan_nr & 0x07)
|
||||
return '%02x00' % b3
|
||||
|
||||
def _bip_response_head(self, pcmd: ProactiveCommand,
|
||||
general_result: str = 'performed_successfully',
|
||||
additional_information: str = ''):
|
||||
"""CommandDetails / DeviceIdentities / Result head part of a BIP TERMINAL
|
||||
RESPONSE. Built on prepare_response() but with two changes:
|
||||
|
||||
- Device identities forced source=terminal, dest=UICC.
|
||||
TS 102 223 6.8.2 mandates for every TERMINAL RESPONSE
|
||||
prepare_response() inverts the commands device id, which is
|
||||
right for a uicc->terminal command but would yield a wrong
|
||||
channel_N->UICC for the channel addressed BIP commands.
|
||||
|
||||
- Result is recreated for non success cases. prepare_response()
|
||||
hard codes empty "additional information", but for enum results
|
||||
like BIP error -> AddlInfoBip the empty value cannot be encoded at
|
||||
all, so we always ask prepare_response() for a success Result
|
||||
and swap for a properly encoded one here."""
|
||||
head = self.prepare_response(pcmd, 'performed_successfully')
|
||||
for i, ie in enumerate(head):
|
||||
if isinstance(ie, DeviceIdentities):
|
||||
head[i] = DeviceIdentities(decoded={'source_dev_id': 'terminal',
|
||||
'dest_dev_id': 'uicc'})
|
||||
elif isinstance(ie, Result) and general_result != 'performed_successfully':
|
||||
res = Result()
|
||||
res.from_dict({'result': {'general_result': general_result,
|
||||
'additional_information': additional_information}})
|
||||
head[i] = res
|
||||
return head
|
||||
|
||||
def _build_data_available_envelope(self, chan: ProactChannel) -> bytes:
|
||||
"""TS 102 223 7.5.10.2 ENVELOPE EVENT DOWNLOAD
|
||||
Event list, Device id terminal->UICC, Channel status,
|
||||
Channel data length (bytes available or FF for > 255)."""
|
||||
avail = min(chan.available_rx(), 0xff)
|
||||
ed = EventDownload(children=[
|
||||
EventList(decoded=['data_available']),
|
||||
DeviceIdentities(decoded={'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}),
|
||||
ChannelStatus(decoded=self._channel_status(chan.chan_nr)),
|
||||
ChannelDataLength(decoded=avail),
|
||||
])
|
||||
return ed.to_tlv()
|
||||
|
||||
def _on_channel_data_available(self, chan: ProactChannel):
|
||||
"""rx reader thread hook: socket data arrived while the channel buffer
|
||||
was empty. card uses ENVELOPE EVENT DOWNLOAD + responds by FETCHing RECEIVE DATA
|
||||
proactive command. Card free only builds and logs"""
|
||||
envelope_hex = b2h(self._build_data_available_envelope(chan))
|
||||
logger.info("channel %u: %u byte(s) available -> ENVELOPE(Data available) %s",
|
||||
chan.chan_nr, chan.available_rx(), envelope_hex)
|
||||
if self.data_available_sink:
|
||||
self.data_available_sink(envelope_hex)
|
||||
|
||||
# handle_*: called by the transport with the decoded proactive command, the returned IE
|
||||
# list becomes the TERMINAL RESPONSE.
|
||||
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'}}
|
||||
# ]}
|
||||
"""SEND SHORT MESSAGE: hand the MO-SMS to sms_sink, answer with success so the card
|
||||
continues with the next part of a multi part response."""
|
||||
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)
|
||||
# Return a successful TERMINAL RESPONSE.
|
||||
# This is important:
|
||||
# - without it the transport cannot complete the proactive command
|
||||
# - for a multi part OTA response, the card would never be asked to give us
|
||||
# the remaining SMS chunks.
|
||||
# 'pcmd' is a decoded SendShortMessage IE, which contains CommandDetails and
|
||||
# DeviceIdentities that prepare_response() echoes/inverts.
|
||||
return self.prepare_response(pcmd)
|
||||
|
||||
def handle_OpenChannel(self, pcmd: ProactiveCommand):
|
||||
"""OPEN CHANNEL: connect a TCP socket to the given address and port, allocate a
|
||||
channel number and report it in the Channel status of the response."""
|
||||
# {'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)
|
||||
|
||||
def refuse(additional_information: str, chan_nr: int = 0):
|
||||
"""TERMINAL RESPONSE refusing the OPEN CHANNEL
|
||||
|
||||
- always a BIP error, only the cause byte of TS 102 223 8.12.11 differs
|
||||
- chan_nr 0 -> "no channel available" in the Channel status, 8.56
|
||||
- 6.8.18, 6.8.20, 6.8.21 want chan status, Bearer desc and buf size
|
||||
in a successful or unsuccessful response
|
||||
"""
|
||||
ies = [ChannelStatus(decoded=self._channel_status(chan_nr, established=False))]
|
||||
ies += [ie for ie in (bearer_desc_ie, buffer_size_ie) if ie is not None]
|
||||
return self._bip_response_head(pcmd, 'bearer_independent_protocol_error',
|
||||
additional_information) + ies
|
||||
|
||||
# UICC/terminal interface transport level is Optional, TS 102 223 6.6.27.x. Absent means
|
||||
# the CAT application runs its own network and transport layer, which we do not do.
|
||||
if transp_lvl_ie is None or transp_lvl_ie.decoded['protocol_type'] != 'tcp_uicc_client_remote':
|
||||
logger.warning("OpenChannel: unsupported UICC/terminal interface transport level (%s) "
|
||||
"-> refusing", transp_lvl_ie.decoded if transp_lvl_ie else '(absent)')
|
||||
return refuse('requested_uicc_if_transp_level_not_available')
|
||||
if other_addr_ie is None or other_addr_ie.decoded.get('type_of_address', None) != 'ipv4':
|
||||
# No cause byte fits a wrong address family. '06' is about the transport level data
|
||||
# object, and 8.12.11 leaves '14' ("IPv4 only allowed") reserved by 3GPP, so '00'.
|
||||
logger.warning("OpenChannel: unsupported data destination address (%s) -> refusing",
|
||||
other_addr_ie.decoded if other_addr_ie else '(absent)')
|
||||
return refuse('no_specific_cause')
|
||||
addr_bytes = h2b(other_addr_ie.decoded['address']) if isinstance(
|
||||
other_addr_ie.decoded['address'], str) else other_addr_ie.decoded['address']
|
||||
ipv4_str = '%u.%u.%u.%u' % (addr_bytes[0], addr_bytes[1], addr_bytes[2], addr_bytes[3])
|
||||
port_nr = transp_lvl_ie.decoded['port_number']
|
||||
logger.info("OpenChannel: connecting to %s:%u", ipv4_str, port_nr)
|
||||
try:
|
||||
channel = self.channels.channel_create()
|
||||
except ValueError:
|
||||
# TS 102 223 6.4.27.2 and 6.4.27.3: no channel left -> BIP error
|
||||
logger.warning("OpenChannel: all %u channels are in use -> refusing",
|
||||
len(self.channels.channels))
|
||||
return refuse('no_channel_availabile')
|
||||
# yes, blocking connect()
|
||||
try:
|
||||
channel.connect(ipv4_str, port_nr)
|
||||
except OSError as e:
|
||||
logger.warning("OpenChannel: connect to %s:%u failed: %s", ipv4_str, port_nr, e)
|
||||
self.channels.channel_delete(channel.chan_nr)
|
||||
# TS 102 223 6.4.30 is the only clause naming a cause for a link that could not be
|
||||
# established: BIP error, channel closed. 6.4.27.4 lists no error cases at all.
|
||||
return refuse('channel_closed', channel.chan_nr)
|
||||
|
||||
# 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._bip_response_head(pcmd) + [
|
||||
ChannelStatus(decoded=self._channel_status(channel.chan_nr)),
|
||||
bearer_desc_ie, buffer_size_ie]
|
||||
|
||||
def handle_CloseChannel(self, pcmd: ProactiveCommand):
|
||||
"""CLOSE CHANNEL: close the socket of the addressed channel and free its number."""
|
||||
logger.info("CloseChannel")
|
||||
logger.info(pcmd)
|
||||
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
|
||||
chan = self._channel_for(dev_id_ie)
|
||||
if chan is None:
|
||||
# channel closed / invalid
|
||||
return self._bip_response_head(pcmd, 'bearer_independent_protocol_error',
|
||||
'channel_id_not_valid')
|
||||
chan.close()
|
||||
return self._bip_response_head(pcmd)
|
||||
|
||||
def handle_ReceiveData(self, pcmd: ProactiveCommand):
|
||||
"""RECEIVE DATA: the card fetches up to Channel data length bytes from the Rx buffer
|
||||
of the addressed channel, the response also carries how many bytes remain."""
|
||||
# {'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)
|
||||
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
|
||||
req_len_ie = Proact._find_first_element_of_type(pcmd.children, ChannelDataLength)
|
||||
chan = self._channel_for(dev_id_ie)
|
||||
if chan is None:
|
||||
return self._bip_response_head(pcmd, 'bearer_independent_protocol_error',
|
||||
'channel_id_not_valid')
|
||||
# TS 102 223 8.54: RECEIVE DATA contains the requested count the card wants
|
||||
requested = req_len_ie.decoded if req_len_ie is not None else chan.available_rx()
|
||||
data, remaining = chan.take_rx(requested)
|
||||
# TS 102 223 6.4.29:
|
||||
# - return data available in the Rx buffer + num bytes still remaining (FF if > 255)
|
||||
# - if fewer than requested available terminal must NOT wait, report and returns what we have
|
||||
general_result = 'performed_successfully'
|
||||
if len(data) < requested:
|
||||
general_result = 'performed_with_missing_information'
|
||||
# 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._bip_response_head(pcmd, general_result) + [
|
||||
ChannelData(decoded=b2h(data)),
|
||||
ChannelDataLength(decoded=min(remaining, 0xff))]
|
||||
|
||||
def handle_SendData(self, pcmd: ProactiveCommand):
|
||||
"""SEND DATA: write the Channel data of the command to the socket of the addressed
|
||||
channel."""
|
||||
# {'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 = self._channel_for(dev_id_ie)
|
||||
if chan is None:
|
||||
return self._bip_response_head(pcmd, 'bearer_independent_protocol_error',
|
||||
'channel_id_not_valid')
|
||||
# lets accept hexstrings as well
|
||||
payload = chan_data_ie.decoded
|
||||
if isinstance(payload, str):
|
||||
payload = h2b(payload)
|
||||
# command_qualifier bit 1 selects 'send immediately' / Tx-buffer store and forward
|
||||
# For TCP stream all we have is a socket and TCP takes care of segmentation,
|
||||
# so just send.
|
||||
chan.send(payload)
|
||||
# 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}
|
||||
# ]
|
||||
# TS 102 223 6.4.30 / 8.54 Channel data length = free space tx buf; FF == > 255 available
|
||||
return self._bip_response_head(pcmd) + [ChannelDataLength(decoded=255)]
|
||||
|
||||
def handle_SetUpEventList(self, pcmd: ProactiveCommand):
|
||||
"""SET UP EVENT LIST: acknowledged, data available and channel status are always on."""
|
||||
# {'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()
|
||||
|
||||
if self.sms_sink is None:
|
||||
logger.info('no sms_sink: dropping MO-SMS %s', deliver_smpp)
|
||||
return
|
||||
self.sms_sink(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)
|
||||
|
||||
|
||||
|
||||
+8
-83
@@ -22,7 +22,7 @@ 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, Adapter
|
||||
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
|
||||
@@ -318,58 +318,11 @@ class FileList(COMPR_TLV_IE, tag=0x92):
|
||||
|
||||
# TS 102 223 Section 8.19
|
||||
class LocationInformation(COMPR_TLV_IE, tag=0x93):
|
||||
# 8.19: coding is per access technology, and the lengths differ (TS 131.111 8.19.1-.4: GERAN 7,
|
||||
# UTRAN/E-UTRAN 9, NG-RAN 11) with nothing in the IE to say which -> keep the value opaque.
|
||||
_construct = GreedyBytes
|
||||
pass
|
||||
|
||||
class MobileIdentityAdapter(Adapter):
|
||||
"""TS 124.008 section 10.5.1.4 figure 10.5.4 + table 10.5.4
|
||||
|
||||
NOT a plain BCD string:
|
||||
- bits 1-3 type of identity + odd/even bit 4
|
||||
- digit 1 in bits 5-8, following octets contain 2 digits, low nibble first
|
||||
- if even length: high nibble of last octet 1111
|
||||
So IMEI IE of 8 bytes is 15 digits + framing nibble."""
|
||||
|
||||
# Table 10.5.4 bits 321
|
||||
TYPE_IMSI = 1
|
||||
TYPE_IMEI = 2
|
||||
TYPE_IMEISV = 3
|
||||
|
||||
def __init__(self, subcon, type_of_identity: int):
|
||||
super().__init__(subcon)
|
||||
self.type_of_identity = type_of_identity
|
||||
|
||||
def _decode(self, obj, context, path):
|
||||
data = bytes(obj)
|
||||
if not data:
|
||||
return ''
|
||||
# TS 24.008 figure 10.5.4: octet 3 holds type of identity (b1-3), odd/even (b4) and
|
||||
# digit 1 in its high nibble, the remaining digits follow BCD swapped from octet 4
|
||||
odd = bool(data[0] & 0x08) # bit 4: 1 = odd number of digits
|
||||
digits = '%x' % (data[0] >> 4) # bits 5-8: digit 1
|
||||
for octet in data[1:]:
|
||||
digits += '%x%x' % (octet & 0x0f, octet >> 4)
|
||||
if not odd:
|
||||
digits = digits[:-1] # drop the 1111 end mark
|
||||
return digits
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
digits = str(obj)
|
||||
odd = len(digits) % 2
|
||||
first = (int(digits[0], 16) << 4) | (0x08 if odd else 0x00) | self.type_of_identity
|
||||
rest = digits[1:] if odd else digits[1:] + 'f'
|
||||
return bytes([first]) + bytes((int(rest[i+1], 16) << 4) | int(rest[i], 16)
|
||||
for i in range(0, len(rest), 2))
|
||||
|
||||
# TS 102 223 Section 8.20, len is fixed at 8: "The IMEI is coded [..] as the
|
||||
# value part of the Mobile Identity IE as specified in TS 124 008", and the
|
||||
# IMEI itself is the 15 digits of TS 123 003.
|
||||
# TS 102 223 Section 8.20
|
||||
class IMEI(COMPR_TLV_IE, tag=0x94):
|
||||
_test_de_encode = [
|
||||
( '94081a32547698103254', '123456789012345' ),
|
||||
]
|
||||
_construct = MobileIdentityAdapter(GreedyBytes, MobileIdentityAdapter.TYPE_IMEI)
|
||||
_construct = BcdAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.21
|
||||
class HelpRequest(COMPR_TLV_IE, tag=0x95):
|
||||
@@ -583,9 +536,9 @@ class Aid(COMPR_TLV_IE, tag=0xAF):
|
||||
|
||||
# TS 102 223 Section 8.61
|
||||
class AccessTechnology(COMPR_TLV_IE, tag=0xBF):
|
||||
SingleAccessTech = Enum(Int8ub, gsm=0, tia_eia_553=1, tia_eia_136_270=2, utran=3, tetra=4,
|
||||
tia_eia_95_b=5, cdma2000_1x=6, cdma2000_hrpd=7, eutran=8,
|
||||
ehrpd=9, nr=0x0a, satellite_nr=0x0b, satellite_eutran=0x0c)
|
||||
SingleAccessTech = Enum(Int8ub, gsm=0, tia_eia_533=1, tia_eia_136_270=2, utran=3, tetra=4,
|
||||
tia_eia_95_b=5, cdma1000_1x=6, cdma2000_hrpd=7, eutran=8,
|
||||
ehrpd=9, nr=0x0a)
|
||||
_construct = GreedyRange(SingleAccessTech)
|
||||
|
||||
# TS 102 223 Section 8.63
|
||||
@@ -643,14 +596,6 @@ class UtranEutranMeasurementQualifier(COMPR_TLV_IE, tag=0xE9):
|
||||
eutran_inter_rat_utran=0x08,
|
||||
eutran_inter_rat_nr=0x09)
|
||||
|
||||
# TS 102 223 Section 8.74, length is not fixed, because IMEISV is 16 digits per TS 123.003
|
||||
# -> even count needs the '1111' end mark and is 9 bytes long
|
||||
class IMEISV(COMPR_TLV_IE, tag=0xE2):
|
||||
_test_de_encode = [
|
||||
( 'e2091332547698103254f6', '1234567890123456' ),
|
||||
]
|
||||
_construct = MobileIdentityAdapter(GreedyBytes, MobileIdentityAdapter.TYPE_IMEISV)
|
||||
|
||||
# TS 102 223 Section 8.75
|
||||
class NetworkSearchMode(COMPR_TLV_IE, tag=0xE5):
|
||||
_construct = Enum(Int8ub, manual=0, automatic=1)
|
||||
@@ -784,12 +729,8 @@ class DnsServerAddress(COMPR_TLV_IE, tag=0xC0):
|
||||
|
||||
# TS 102 223 Section 8.105
|
||||
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
||||
# 2 bytes/entry:
|
||||
# - technology of 8.61
|
||||
# - state byte b1 is 0 disabled/1 enabled
|
||||
# - b2-b8 RFU.
|
||||
AccessTechTuple = Struct('technology'/AccessTechnology.SingleAccessTech,
|
||||
'state'/FlagsEnum(Int8ub, enabled=1))
|
||||
'state'/FlagsEnum(Int8ub, enabled=0))
|
||||
_construct = GreedyRange(AccessTechTuple)
|
||||
|
||||
# TS 102 223 Section 8.107
|
||||
@@ -822,22 +763,6 @@ class SMSPPDownload(BER_TLV_IE, tag=0xD1,
|
||||
nested=[DeviceIdentities, Address, SMS_TPDU]):
|
||||
pass
|
||||
|
||||
|
||||
def sms_pp_download_envelope(tpdu, call_number: str = '0123456') -> SMSPPDownload:
|
||||
"""TS 31.111 Section 7.1.1.2 wrap of a SMS-DELIVER TPDU in the ENVELOPE (SMS-PP Download)
|
||||
call_number :
|
||||
SMSC address to report, defined in TS 31.111 7.1.1.2 as
|
||||
"the RP_Originating_Address of the Service Centre (TS-Service-Centre-Address, 3GPP TS 24.011)"
|
||||
its presence is Conditional, and the note there says the UICC should be fine
|
||||
if its missing, so for remote management its presence should suffice (?).
|
||||
"""
|
||||
return SMSPPDownload(children=[
|
||||
DeviceIdentities(decoded={'source_dev_id': 'network', 'dest_dev_id': 'uicc'}),
|
||||
Address(decoded={'ton_npi': {'ext': False, 'type_of_number': 'unknown',
|
||||
'numbering_plan_id': 'unknown'},
|
||||
'call_number': call_number}),
|
||||
SMS_TPDU(decoded={'tpdu': b2h(tpdu.to_bytes())})])
|
||||
|
||||
# TS 101 220 Table 7.17 + 31.111 7.1.1.3
|
||||
class SMSCBDownload(BER_TLV_IE, tag=0xD2,
|
||||
nested=[DeviceIdentities, CBSPage]):
|
||||
|
||||
@@ -873,6 +873,30 @@ class SdKey(BinaryParam):
|
||||
|
||||
SdKey.all_implementations = []
|
||||
|
||||
transitional_name_mapping = {
|
||||
'SCP02-KVN20-AES-DEK': 'SCP02-20-AES-DEK',
|
||||
'SCP02-KVN20-AES-ENC': 'SCP02-20-AES-ENC',
|
||||
'SCP02-KVN20-AES-MAC': 'SCP02-20-AES-MAC',
|
||||
'SCP02-KVN21-AES-DEK': 'SCP02-21-AES-DEK',
|
||||
'SCP02-KVN21-AES-ENC': 'SCP02-21-AES-ENC',
|
||||
'SCP02-KVN21-AES-MAC': 'SCP02-21-AES-MAC',
|
||||
'SCP02-KVN22-AES-DEK': 'SCP02-22-AES-DEK',
|
||||
'SCP02-KVN22-AES-ENC': 'SCP02-22-AES-ENC',
|
||||
'SCP02-KVN22-AES-MAC': 'SCP02-22-AES-MAC',
|
||||
'SCP02-KVNff-AES-DEK': 'SCP02-ff-AES-DEK',
|
||||
'SCP02-KVNff-AES-ENC': 'SCP02-ff-AES-ENC',
|
||||
'SCP02-KVNff-AES-MAC': 'SCP02-ff-AES-MAC',
|
||||
'SCP03-KVN30-AES-DEK': 'SCP03-30-AES-DEK',
|
||||
'SCP03-KVN30-AES-ENC': 'SCP03-30-AES-ENC',
|
||||
'SCP03-KVN30-AES-MAC': 'SCP03-30-AES-MAC',
|
||||
'SCP03-KVN31-AES-DEK': 'SCP03-31-AES-DEK',
|
||||
'SCP03-KVN31-AES-ENC': 'SCP03-31-AES-ENC',
|
||||
'SCP03-KVN31-AES-MAC': 'SCP03-31-AES-MAC',
|
||||
'SCP03-KVN32-AES-DEK': 'SCP03-32-AES-DEK',
|
||||
'SCP03-KVN32-AES-ENC': 'SCP03-32-AES-ENC',
|
||||
'SCP03-KVN32-AES-MAC': 'SCP03-32-AES-MAC',
|
||||
}
|
||||
|
||||
def camel(s):
|
||||
return s[:1].upper() + s[1:].lower()
|
||||
|
||||
@@ -904,6 +928,8 @@ class SdKey(BinaryParam):
|
||||
|
||||
max_key_len = attrs.get('allow_len')[-1]
|
||||
|
||||
cls_label = transitional_name_mapping.get(cls_label, cls_label)
|
||||
|
||||
attrs.update({
|
||||
'name' : cls_label,
|
||||
'kvn': kvn,
|
||||
|
||||
+3
-4
@@ -38,16 +38,15 @@ class SwMatchError(Exception):
|
||||
"""Raised when an operation specifies an expected SW but the actual SW from
|
||||
the card doesn't match."""
|
||||
|
||||
def __init__(self, sw_actual: str, sw_expected, rs=None):
|
||||
def __init__(self, sw_actual: str, sw_expected: str, rs=None):
|
||||
"""
|
||||
Args:
|
||||
sw_actual : the SW we actually received from the card (4 hex digits)
|
||||
sw_expected : the SW we expected to receive from the card (4 hex digits),
|
||||
or a list of acceptable ones
|
||||
sw_expected : the SW we expected to receive from the card (4 hex digits)
|
||||
rs : interpreter class to convert SW to string
|
||||
"""
|
||||
self.sw_actual = sw_actual
|
||||
self.sw_expected = '/'.join(sw_expected) if isinstance(sw_expected, (list, tuple)) else sw_expected
|
||||
self.sw_expected = sw_expected
|
||||
self.rs = rs
|
||||
|
||||
@property
|
||||
|
||||
+128
-360
@@ -18,12 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import io
|
||||
import hashlib
|
||||
from copy import deepcopy
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
from construct import Optional as COptional
|
||||
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
|
||||
from construct import Construct, stream_read, stream_write
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from Cryptodome.Cipher import DES, DES3, AES
|
||||
from osmocom.utils import *
|
||||
@@ -31,15 +29,12 @@ from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
from pySim.utils import ResTuple
|
||||
from pySim.card_key_provider import card_key_provider_get_field
|
||||
from pySim.global_platform.scp import SCP, SCP02, SCP03
|
||||
from pySim.global_platform.scp import SCP02, SCP03
|
||||
from pySim.global_platform.install_param import gen_install_parameters
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
|
||||
from pySim.javacard import CapFile
|
||||
from pySim.log import PySimLogger
|
||||
|
||||
log = PySimLogger.get(__name__)
|
||||
|
||||
# GPCS Table 11-48 Load Parameter Tags
|
||||
class NonVolatileCodeMinMemoryReq(BER_TLV_IE, tag=0xC6):
|
||||
@@ -153,24 +148,6 @@ sw_table = {
|
||||
},
|
||||
}
|
||||
|
||||
class PutKeyLength(Construct):
|
||||
"""A length field of a PUT KEY data field, GP CardSpec v2.3.1 11.8.2.3.1
|
||||
- all lengths ASN.1 BER-TLV (ITU-T X.690 Section 8.1.3)
|
||||
- except that the length 128 may also be coded on one byte as '80' for backwards compatibility
|
||||
80 does not introduce the indefinite form here which is unused in GP as far as i know.
|
||||
That legacy form is accepted when parsing, but never generated, which agrees with the spec"""
|
||||
def _parse(self, stream, context, path):
|
||||
first = stream_read(stream, 1, path)[0]
|
||||
if first <= 0x80:
|
||||
return first
|
||||
return int.from_bytes(stream_read(stream, first & 0x7f, path), 'big')
|
||||
|
||||
def _build(self, obj, stream, context, path):
|
||||
data = bertlv_encode_len(obj)
|
||||
stream_write(stream, data, len(data), path)
|
||||
return obj
|
||||
|
||||
|
||||
# GlobalPlatform 2.1.1 Section 9.1.6
|
||||
KeyType = Enum(Byte, des=0x80,
|
||||
tls_psk=0x85, # v2.3.1 Section 11.1.8
|
||||
@@ -535,63 +512,6 @@ class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCy
|
||||
ExecutableModuleAID, AssociatedSecurityDomainAID]):
|
||||
pass
|
||||
|
||||
# GP CS v2.3.1 Table 11-36/11-37 possible data objects requested/returned from GET STATUS for each registry entry.
|
||||
# Applications and Executable Load Files have _different_ sets, so a tag list requesting them has
|
||||
# to match the subset because 11.4.2.3 warns that asking for a data object an entry does not have
|
||||
# "may" be answered with an error status.
|
||||
GetStatusTagListIEs = {
|
||||
# Table 11-36 GP Application Data
|
||||
'isd': [ApplicationAID, LifeCycleState, Privileges, ImplicitSelectionParameter,
|
||||
ExecutableLoadFileAID, AssociatedSecurityDomainAID],
|
||||
'applications': [ApplicationAID, LifeCycleState, Privileges, ImplicitSelectionParameter,
|
||||
ExecutableLoadFileAID, AssociatedSecurityDomainAID],
|
||||
# Table 11-37 GP Executable Load File Data. 84 only for the subset that asks for the modules (Note 2)!
|
||||
'files': [ApplicationAID, LifeCycleState, ExecutableLoadFileVersionNumber,
|
||||
AssociatedSecurityDomainAID],
|
||||
'files_and_modules': [ApplicationAID, LifeCycleState, ExecutableLoadFileVersionNumber,
|
||||
ExecutableModuleAID, AssociatedSecurityDomainAID],
|
||||
}
|
||||
|
||||
def get_status_tag_list(subset: str) -> bytes:
|
||||
"""Encode the GET STATUS tag list for the given status subset"""
|
||||
tags = b''.join([bertlv_encode_tag(ie.tag) for ie in GetStatusTagListIEs[subset]])
|
||||
return b'\x5c' + bertlv_encode_len(len(tags)) + tags
|
||||
|
||||
# GP CS v2.3.1 Appendix H.2 / Table H-1
|
||||
# oid prefix {iso(1) member-body(2) country-USA(840) globalPlatform(114283)} + card management type 2
|
||||
# afterwards GP version.
|
||||
OID_GP_CARD_MGMT_TYPE = h2b('2a864886fc6b02')
|
||||
|
||||
def _find_tlv_value(decoded, key: str):
|
||||
"""depth first search for the nested decoded TLV_IE dict/list"""
|
||||
if isinstance(decoded, dict):
|
||||
for k, v in decoded.items():
|
||||
if k == key:
|
||||
return v
|
||||
found = _find_tlv_value(v, key)
|
||||
if found is not None:
|
||||
return found
|
||||
elif isinstance(decoded, list):
|
||||
for item in decoded:
|
||||
found = _find_tlv_value(item, key)
|
||||
if found is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
def decode_gp_version(card_data: bytes) -> Optional[Tuple[int, ...]]:
|
||||
"""GP version from Card Data returned by GET DATA, like (2, 1, 1) or (2, 2).
|
||||
None if cm type OID is absent/unknown"""
|
||||
cd = CardData()
|
||||
cd.from_tlv(card_data)
|
||||
ctv = _find_tlv_value(cd.to_dict(), 'card_management_type_and_version')
|
||||
oid = _find_tlv_value(ctv, 'object_identifier') if ctv is not None else None
|
||||
if oid is None:
|
||||
return None
|
||||
oid = h2b(oid) if isinstance(oid, str) else bytes(oid)
|
||||
if not oid.startswith(OID_GP_CARD_MGMT_TYPE):
|
||||
return None
|
||||
return tuple(oid[len(OID_GP_CARD_MGMT_TYPE):])
|
||||
|
||||
# Application Dedicated File of a Security Domain
|
||||
class ADF_SD(CardADF):
|
||||
StoreData = BitStruct('last_block'/Flag,
|
||||
@@ -607,241 +527,6 @@ class ADF_SD(CardADF):
|
||||
def decode_select_response(self, data_hex: str) -> object:
|
||||
return decode_select_response(data_hex)
|
||||
|
||||
@staticmethod
|
||||
def store_data(scc: SimCardCommands, data: bytes, structure:str = 'none', encryption:str = 'none',
|
||||
response_permitted: bool = False) -> bytes:
|
||||
"""
|
||||
Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details.
|
||||
"""
|
||||
max_cmd_len =scc.max_cmd_len
|
||||
# Table 11-89 of GP Card Specification v2.3
|
||||
remainder = data
|
||||
block_nr = 0
|
||||
response = ''
|
||||
while len(remainder):
|
||||
chunk = remainder[:max_cmd_len]
|
||||
remainder = remainder[max_cmd_len:]
|
||||
p1b = build_construct(ADF_SD.StoreData,
|
||||
{'last_block': len(remainder) == 0, 'encryption': encryption,
|
||||
'structure': structure, 'response': response_permitted})
|
||||
hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk))
|
||||
data, _sw =scc.send_apdu_checksw(hdr + b2h(chunk) + "00")
|
||||
block_nr += 1
|
||||
response += data
|
||||
return h2b(response)
|
||||
|
||||
@staticmethod
|
||||
def get_data(scc: SimCardCommands, tag: int) -> bytes:
|
||||
(data, _sw) = scc.get_data(cla=0x80, tag=tag)
|
||||
return data
|
||||
|
||||
# Table 11-68: Key Data Field - Format 1 (Basic Format). The key component block length is
|
||||
# BER-TLV coded (Section 11.8.2.3.1), the key check value length is always '00' - '7F'.
|
||||
KeyDataBasic = Struct('key_type'/KeyType,
|
||||
'kcb'/Prefixed(PutKeyLength(), GreedyBytes),
|
||||
'kcv'/Prefixed(Int8ub, GreedyBytes))
|
||||
|
||||
@staticmethod
|
||||
def encode_key_data_basic(key_type: str, kcb: bytes, kcv: bytes) -> bytes:
|
||||
"""Generic Basic key data field, GP CardSpec v2.3 Table 11-68):
|
||||
tag || L1 || <maybe L2> KCB || <1-byte length> KCV"""
|
||||
return ADF_SD.KeyDataBasic.build({'key_type': key_type, 'kcb': kcb, 'kcv': kcv})
|
||||
|
||||
@staticmethod
|
||||
def encode_key_data_psk(clear_key: bytes, ciphered_key: bytes, kcv: bytes) -> bytes:
|
||||
"""Single PSK TLS '85' key data field per GP Amendment B 1.2, 3.9.1 / Table 3-13:
|
||||
85 | L1 | <L2> <ciphered PSK key> | <KCV length> | <KCV>
|
||||
- framing is like Basic Format, but the kcb is always GP CardSpec Table 11-70
|
||||
so always with the length of the clear text key value, even without padding!
|
||||
- 'ciphered_key' is DEK(block-padded clear key), no additional length prefix."""
|
||||
kcb = bertlv_encode_len(len(clear_key)) + ciphered_key
|
||||
return ADF_SD.encode_key_data_basic('tls_psk', kcb, kcv)
|
||||
|
||||
@staticmethod
|
||||
def build_put_key_data(kvn: int, keys: List[dict], scp) -> bytes:
|
||||
"""Assemble the PUT KEY data field, mixed PSK + DES DEK is supported:
|
||||
- new KVN followed by one key data field per key.
|
||||
- tls_psk keys per GP Amendment B
|
||||
- other key types generic Basic format
|
||||
Param 'keys' is a dict:
|
||||
- 'key_type' (str)
|
||||
- 'clear_key' (bytes)
|
||||
- 'kcv' (bytes / empty).
|
||||
'scp' may be None (e.g. during personalization, when the DEK may not be required)."""
|
||||
key_data = kvn.to_bytes(1, 'big')
|
||||
for k in keys:
|
||||
clear = k['clear_key']
|
||||
if k['key_type'] == 'tls_psk':
|
||||
# len always part of the data see CardSpec Table 11-70 vs Table 11-71
|
||||
if scp:
|
||||
ciphered = scp.dek_encrypt(scp.pad_to_blocksize(clear))
|
||||
else:
|
||||
ciphered = clear
|
||||
key_data += ADF_SD.encode_key_data_psk(clear, ciphered, k['kcv'])
|
||||
else:
|
||||
if scp:
|
||||
ciphered = scp.encrypt_key(clear)
|
||||
else:
|
||||
# (for example) during personalization, DEK might not be required
|
||||
ciphered = clear
|
||||
key_data += ADF_SD.encode_key_data_basic(k['key_type'], ciphered, k['kcv'])
|
||||
return key_data
|
||||
|
||||
@staticmethod
|
||||
def put_key(scc: SimCardCommands, old_kvn:int, kvn: int, kid: int, keys: List[dict]) -> bytes:
|
||||
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details."""
|
||||
key_data = ADF_SD.build_put_key_data(kvn, keys, scc.scp)
|
||||
# Lc of Table 11-64 is a single byte, while LOAD or STORE DATA splits we can't:
|
||||
# 11.8.2.3.3 splits a key at component boundaries -> not helping here
|
||||
max_cmd_len = scc.max_cmd_len
|
||||
if len(key_data) > max_cmd_len:
|
||||
raise ValueError('key data field of %u bytes exceeds the maximum command length of %u '
|
||||
'(limited by the overhead of the current secure channel); use fewer '
|
||||
'keys per command, a single key component that large needs STORE DATA' %
|
||||
(len(key_data), max_cmd_len))
|
||||
hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
|
||||
data, _sw = scc.send_apdu_checksw(hdr + b2h(key_data) + "00")
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def gp_version(scc: SimCardCommands) -> Optional[Tuple[int, ...]]:
|
||||
"""GP version the selected SD reports in its Card Recognition
|
||||
Data, e.g. (2, 1, 1). Card Recognition Data "shall be present" v2.1.1/v2.3.1 section 7.4.1.3,
|
||||
so this must succeed no matter the GP version. None if card did not answer GET DATA / OID unknown.
|
||||
Cached, it cannot change during a session."""
|
||||
version = None
|
||||
try:
|
||||
data, _sw = scc.get_data(cla=0x80, tag=CardData.tag)
|
||||
version = decode_gp_version(h2b(data))
|
||||
log.debug("Card Recognition Data reports GlobalPlatform %s",
|
||||
'.'.join(str(v) for v in version) if version else 'unknown')
|
||||
except (SwMatchError, ValueError) as e:
|
||||
log.warning("Could not determine GlobalPlatform version: %s", e)
|
||||
return version
|
||||
|
||||
@staticmethod
|
||||
def get_status(scc: SimCardCommands, subset:str, aid_search_qualifier:Hexstr = '',
|
||||
version:Optional[Tuple[int, ...]] = None) -> List[GpRegistryRelatedData]:
|
||||
aid = ApplicationAID(decoded=aid_search_qualifier)
|
||||
# GPC CardSpec v2.3.1 Table 11-35 says only the AID search tag is mandatory, tag list is
|
||||
# Optional and not present in the older v2.1.1, where section 9.4.2.3 defines the data
|
||||
# field as the search qualifier.
|
||||
# Cards like the sja5 implementing that old GP version reject anything else with 6A80
|
||||
# from v2.1.1 Table 9-26 so only send a tag list to a card that announces v2.2 or later.
|
||||
#
|
||||
# Not sending one is not a problem on older cards, the tag list only gives us data beyond
|
||||
# what 11.4.3.1 gives us anyway, for example the associated SD AID which matters on an eUICC
|
||||
# where entries belong to different SD.
|
||||
if version is not None and version >= (2, 2):
|
||||
try:
|
||||
return ADF_SD._get_status(scc, subset, aid.to_tlv() + get_status_tag_list(subset))
|
||||
except SwMatchError as e:
|
||||
# Retry if v2.2 or later but rejected the tag list anyway.
|
||||
# 6A80 and 6A88 are the error conditions GET STATUS defines in table 11-39.
|
||||
# Retrying beats not ending up with a list again...
|
||||
if e.sw_actual not in ('6a80', '6a88'):
|
||||
raise
|
||||
log.warning("Card reports GlobalPlatform %s but answered %s to the GET STATUS tag list; "
|
||||
"retrying with the default search",
|
||||
'.'.join(str(v) for v in version), e.sw_actual)
|
||||
return ADF_SD._get_status(scc, subset, aid.to_tlv(), empty_on_6a88=True)
|
||||
|
||||
@staticmethod
|
||||
def _get_status(scc: SimCardCommands, subset:str, cmd_data:bytes,
|
||||
empty_on_6a88: bool = False) -> List[GpRegistryRelatedData]:
|
||||
subset_hex = b2h(build_construct(StatusSubset, subset))
|
||||
p2 = 0x02 # GPC v2.3.1 11.4.2.2 table 11-34, b2: response data structure per table 11-36
|
||||
grd_list = []
|
||||
while True:
|
||||
hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
|
||||
data, sw = scc.send_apdu(hdr + b2h(cmd_data) + "00")
|
||||
if sw == '6a88':
|
||||
# Table 11-39 "Referenced data not found". After collecting all pages this can
|
||||
# only mean "nothing more matches" -> listing is complete. On the first page
|
||||
# it is ambiguous, empty result or bad command data field, so leave that to get_status()
|
||||
# which knows if a tag list was sent.
|
||||
if grd_list or empty_on_6a88:
|
||||
return grd_list
|
||||
raise SwMatchError(sw, ['9000', '6310'])
|
||||
if sw not in ['9000', '6310']:
|
||||
# Never return a silently truncated registry
|
||||
raise SwMatchError(sw, ['9000', '6310'])
|
||||
remainder = h2b(data)
|
||||
while len(remainder):
|
||||
# tlv sequence, each element is one GpRegistryRelatedData()
|
||||
grd = GpRegistryRelatedData()
|
||||
_dec, remainder = grd.from_tlv(remainder)
|
||||
grd_list.append(grd)
|
||||
if sw == '9000':
|
||||
return grd_list
|
||||
# 6310 = more data available, table 11-38: reissue as get next occurrence(s), b1 of
|
||||
# table 11-34. Keeps b2 unchanged.
|
||||
p2 |= 0x01
|
||||
|
||||
@staticmethod
|
||||
def set_status(scc: SimCardCommands, scope:str, status:str, aid:Hexstr = ''):
|
||||
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
|
||||
'scope'/SetStatusScope, 'status'/CLifeCycleState,
|
||||
'aid'/Prefixed(Int8ub, COptional(GreedyBytes)))
|
||||
apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
|
||||
_data, _sw =scc.send_apdu_checksw(b2h(apdu))
|
||||
|
||||
@staticmethod
|
||||
def install(scc: SimCardCommands, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
|
||||
return scc.send_apdu_checksw(cmd_hex)
|
||||
|
||||
@staticmethod
|
||||
def delete(scc: SimCardCommands, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E4%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
|
||||
return scc.send_apdu_checksw(cmd_hex)
|
||||
|
||||
@staticmethod
|
||||
def load(scc: SimCardCommands, contents:bytes, chunk_len:Optional[int] = None):
|
||||
# scc.max_cmd_len knows the overhead the currently active SCP
|
||||
# 240 is the old default, keep it for now.
|
||||
max_chunk_len = scc.max_cmd_len
|
||||
if chunk_len is None:
|
||||
chunk_len = min(240, max_chunk_len)
|
||||
elif not 1 <= chunk_len <= max_chunk_len:
|
||||
raise ValueError('chunk_len must be in range 1..%u (limited by the overhead of the current secure channel)' %
|
||||
max_chunk_len)
|
||||
# build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case
|
||||
remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents
|
||||
# transfer this in various chunks to the card
|
||||
total_size = len(remainder)
|
||||
block_nr = 0
|
||||
while len(remainder):
|
||||
block = remainder[:chunk_len]
|
||||
remainder = remainder[chunk_len:]
|
||||
# build LOAD command APDU according to GPC_SPE_034 section 11.6.2 / Table 11-56
|
||||
p1 = 0x00 if len(remainder) else 0x80
|
||||
p2 = block_nr % 256
|
||||
block_nr += 1
|
||||
cmd_hex = "80E8%02x%02x%02x%s00" % (p1, p2, len(block), b2h(block))
|
||||
_rsp_hex, _sw = scc.send_apdu_checksw(cmd_hex)
|
||||
log.info("Loaded a total of %u bytes in %u blocks. Don't forget install_for_install (and make selectable) now!",
|
||||
total_size, block_nr)
|
||||
|
||||
@staticmethod
|
||||
def establish_scp(scc: SimCardCommands, scp: SCP, host_challenge: Optional[bytes] = None,
|
||||
security_level: int = 0x01):
|
||||
# perform the common functionality shared by SCP02 and SCP03 establishment
|
||||
init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge)
|
||||
init_update_resp, _sw =scc.send_apdu_checksw(b2h(init_update_apdu))
|
||||
scp.parse_init_update_resp(h2b(init_update_resp))
|
||||
ext_auth_apdu = scp.gen_ext_auth_apdu(security_level)
|
||||
_ext_auth_resp, _sw =scc.send_apdu_checksw(b2h(ext_auth_apdu))
|
||||
log.info("Successfully established a %s secure channel", str(scp))
|
||||
# store a reference to the SCP instance
|
||||
scc.scp = scp
|
||||
|
||||
@staticmethod
|
||||
def release_scp(scc: SimCardCommands):
|
||||
scc.scp = None
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
get_data_parser = argparse.ArgumentParser()
|
||||
@@ -859,8 +544,7 @@ class ADF_SD(CardADF):
|
||||
self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name,
|
||||
do_names))
|
||||
return
|
||||
|
||||
data = ADF_SD.get_data(self._cmd.lchan.scc, tag=tlv_cls.tag)
|
||||
(data, _sw) = self._cmd.lchan.scc.get_data(cla=0x80, tag=tlv_cls.tag)
|
||||
ie = tlv_cls()
|
||||
ie.from_tlv(h2b(data))
|
||||
self._cmd.poutput_json(ie.to_dict())
|
||||
@@ -881,8 +565,27 @@ class ADF_SD(CardADF):
|
||||
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
|
||||
response_permitted = opts.response == 'may_be_returned'
|
||||
ADF_SD.store_data(self._cmd.lchan.scc, h2b(opts.DATA), opts.data_structure, opts.encryption,
|
||||
response_permitted)
|
||||
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
|
||||
|
||||
def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes:
|
||||
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
|
||||
max_cmd_len = self._cmd.lchan.scc.max_cmd_len
|
||||
# Table 11-89 of GP Card Specification v2.3
|
||||
remainder = data
|
||||
block_nr = 0
|
||||
response = ''
|
||||
while len(remainder):
|
||||
chunk = remainder[:max_cmd_len]
|
||||
remainder = remainder[max_cmd_len:]
|
||||
p1b = build_construct(ADF_SD.StoreData,
|
||||
{'last_block': len(remainder) == 0, 'encryption': encryption,
|
||||
'structure': structure, 'response': response_permitted})
|
||||
hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk))
|
||||
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00")
|
||||
block_nr += 1
|
||||
response += data
|
||||
return h2b(response)
|
||||
|
||||
put_key_parser = argparse.ArgumentParser()
|
||||
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')
|
||||
@@ -899,8 +602,8 @@ class ADF_SD(CardADF):
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
|
||||
|
||||
The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
|
||||
otherwise be automatically generated for DES, AES and TLS-PSK keys. You can suppress the
|
||||
latter using `--suppress-key-check`.
|
||||
otherwise be automatically generated for DES and AES keys. You can suppress the latter using
|
||||
`--suppress-key-check`.
|
||||
|
||||
Example (SCP80 KIC/KID/KIK):
|
||||
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
|
||||
@@ -917,17 +620,36 @@ class ADF_SD(CardADF):
|
||||
kdb = []
|
||||
for i in range(0, len(opts.key_type)):
|
||||
if opts.key_check and len(opts.key_check) > i:
|
||||
kcv = h2b(opts.key_check[i])
|
||||
kcv = opts.key_check[i]
|
||||
elif opts.suppress_key_check:
|
||||
kcv = b''
|
||||
kcv = ''
|
||||
else:
|
||||
kcv = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
|
||||
kdb.append({'key_type': opts.key_type[i], 'clear_key': h2b(opts.key_data[i]), 'kcv': kcv})
|
||||
kcv_bin = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
|
||||
kcv = b2h(kcv_bin)
|
||||
if self._cmd.lchan.scc.scp:
|
||||
# encrypted key data with DEK of current SCP
|
||||
kcb = b2h(self._cmd.lchan.scc.scp.encrypt_key(h2b(opts.key_data[i])))
|
||||
else:
|
||||
# (for example) during personalization, DEK might not be required)
|
||||
kcb = opts.key_data[i]
|
||||
kdb.append({'key_type': opts.key_type[i], 'kcb': kcb, 'kcv': kcv})
|
||||
p2 = opts.key_id
|
||||
if len(opts.key_type) > 1:
|
||||
p2 |= 0x80
|
||||
ADF_SD.put_key(self._cmd.lchan.scc, opts.old_key_version_nr, opts.key_version_nr, p2, kdb)
|
||||
self.put_key(opts.old_key_version_nr, opts.key_version_nr, p2, kdb)
|
||||
|
||||
# Table 11-68: Key Data Field - Format 1 (Basic Format)
|
||||
KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
|
||||
'kcb'/Prefixed(Int8ub, GreedyBytes),
|
||||
'kcv'/Prefixed(Int8ub, GreedyBytes)))
|
||||
|
||||
def put_key(self, old_kvn:int, kvn: int, kid: int, key_dict: dict) -> bytes:
|
||||
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
|
||||
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details."""
|
||||
key_data = kvn.to_bytes(1, 'big') + build_construct(ADF_SD.AddlShellCommands.KeyDataBasic, key_dict)
|
||||
hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
|
||||
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data) + "00")
|
||||
return data
|
||||
|
||||
get_status_parser = argparse.ArgumentParser()
|
||||
get_status_parser.add_argument('subset', choices=list(StatusSubset.ksymapping.values()),
|
||||
@@ -939,18 +661,30 @@ class ADF_SD(CardADF):
|
||||
def do_get_status(self, opts):
|
||||
"""Perform GlobalPlatform GET STATUS command in order to retrieve status information
|
||||
on Issuer Security Domain, Executable Load File, Executable Module or Applications."""
|
||||
grd_list = ADF_SD.get_status(self._cmd.lchan.scc, opts.subset, opts.aid, self.gp_version())
|
||||
grd_list = self.get_status(opts.subset, opts.aid)
|
||||
for grd in grd_list:
|
||||
self._cmd.poutput_json(grd.to_dict())
|
||||
|
||||
def gp_version(self) -> Optional[Tuple[int, ...]]:
|
||||
"""GP version the selected SD reports in its Card Recognition
|
||||
Data, e.g. (2, 1, 1). Card Recognition Data "shall be present" v2.1.1/v2.3.1 section 7.4.1.3,
|
||||
so this must succeed no matter the GP version. None if card did not answer GET DATA / OID unknown.
|
||||
Cached, it cannot change during a session."""
|
||||
if not hasattr(self, '_gp_version'):
|
||||
self._gp_version = ADF_SD.gp_version(self._cmd.lchan.scc)
|
||||
return self._gp_version
|
||||
def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpRegistryRelatedData]:
|
||||
subset_hex = b2h(build_construct(StatusSubset, subset))
|
||||
aid = ApplicationAID(decoded=aid_search_qualifier)
|
||||
cmd_data = aid.to_tlv() + h2b('5c054f9f70c5cc')
|
||||
p2 = 0x02 # TLV format according to Table 11-36
|
||||
grd_list = []
|
||||
while True:
|
||||
hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
|
||||
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data) + "00")
|
||||
remainder = h2b(data)
|
||||
while len(remainder):
|
||||
# tlv sequence, each element is one GpRegistryRelatedData()
|
||||
grd = GpRegistryRelatedData()
|
||||
_dec, remainder = grd.from_tlv(remainder)
|
||||
grd_list.append(grd)
|
||||
if sw != '6310':
|
||||
return grd_list
|
||||
else:
|
||||
p2 |= 0x01
|
||||
return grd_list
|
||||
|
||||
set_status_parser = argparse.ArgumentParser()
|
||||
set_status_parser.add_argument('scope', choices=list(SetStatusScope.ksymapping.values()),
|
||||
@@ -965,7 +699,14 @@ class ADF_SD(CardADF):
|
||||
"""Perform GlobalPlatform SET STATUS command in order to change the life cycle state of the
|
||||
Issuer Security Domain, Supplementary Security Domain or Application. This normally requires
|
||||
prior authentication with a Secure Channel Protocol."""
|
||||
ADF_SD.set_status(self._cmd.lchan.scc, opts.scope, opts.status, opts.aid)
|
||||
self.set_status(opts.scope, opts.status, opts.aid)
|
||||
|
||||
def set_status(self, scope:str, status:str, aid:Hexstr = ''):
|
||||
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
|
||||
'scope'/SetStatusScope, 'status'/CLifeCycleState,
|
||||
'aid'/Prefixed(Int8ub, COptional(GreedyBytes)))
|
||||
apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
|
||||
_data, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu))
|
||||
|
||||
inst_perso_parser = argparse.ArgumentParser()
|
||||
inst_perso_parser.add_argument('application_aid', type=is_hexstr, help='Application AID')
|
||||
@@ -975,8 +716,7 @@ class ADF_SD(CardADF):
|
||||
"""Perform GlobalPlatform INSTALL [for personalization] command in order to inform a Security
|
||||
Domain that the following STORE DATA commands are meant for a specific AID (specified here)."""
|
||||
# Section 11.5.2.3.6 / Table 11-47
|
||||
ADF_SD.install(self._cmd.lchan.scc, 0x20, 0x00, "0000%02x%s000000" %
|
||||
(len(opts.application_aid)//2, opts.application_aid))
|
||||
self.install(0x20, 0x00, "0000%02x%s000000" % (len(opts.application_aid)//2, opts.application_aid))
|
||||
|
||||
inst_inst_parser = argparse.ArgumentParser()
|
||||
inst_inst_parser.add_argument('--load-file-aid', type=is_hexstr, default='',
|
||||
@@ -1011,7 +751,7 @@ class ADF_SD(CardADF):
|
||||
# convert from list to "true-dict" as required by construct.FlagsEnum
|
||||
decoded['privileges'] = {x: True for x in decoded['privileges']}
|
||||
ifi_bytes = build_construct(InstallForInstallCD, decoded)
|
||||
ADF_SD.install(self._cmd.lchan.scc, p1, 0x00, b2h(ifi_bytes))
|
||||
self.install(p1, 0x00, b2h(ifi_bytes))
|
||||
|
||||
inst_load_parser = argparse.ArgumentParser()
|
||||
inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True,
|
||||
@@ -1036,7 +776,11 @@ class ADF_SD(CardADF):
|
||||
'load_parameters'/Prefixed(Int8ub, GreedyBytes),
|
||||
'load_token'/Prefixed(Int8ub, GreedyBytes))
|
||||
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
|
||||
ADF_SD.install(self._cmd.lchan.scc, 0x02, 0x00, b2h(ifl_bytes))
|
||||
self.install(0x02, 0x00, b2h(ifl_bytes))
|
||||
|
||||
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
|
||||
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
|
||||
del_cc_parser = argparse.ArgumentParser()
|
||||
del_cc_parser.add_argument('aid', type=is_hexstr,
|
||||
@@ -1050,7 +794,7 @@ class ADF_SD(CardADF):
|
||||
File, an Application or an Executable Load File and its related Applications."""
|
||||
p2 = 0x80 if opts.delete_related_objects else 0x00
|
||||
aid = ApplicationAID(decoded=opts.aid)
|
||||
ADF_SD.delete(self._cmd.lchan.scc, 0x00, p2, b2h(aid.to_tlv()))
|
||||
self.delete(0x00, p2, b2h(aid.to_tlv()))
|
||||
|
||||
del_key_parser = argparse.ArgumentParser()
|
||||
del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)')
|
||||
@@ -1071,30 +815,50 @@ class ADF_SD(CardADF):
|
||||
cmd += "d001%02x" % opts.key_id
|
||||
if opts.key_ver is not None:
|
||||
cmd += "d201%02x" % opts.key_ver
|
||||
ADF_SD.delete(self._cmd.lchan.scc, 0x00, p2, cmd)
|
||||
self.delete(0x00, p2, cmd)
|
||||
|
||||
def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E4%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
|
||||
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
|
||||
load_parser = argparse.ArgumentParser()
|
||||
load_parser_from_grp = load_parser.add_mutually_exclusive_group(required=True)
|
||||
load_parser_from_grp.add_argument('--from-hex', type=is_hexstr, help='load from hex string')
|
||||
load_parser_from_grp.add_argument('--from-file', type=argparse.FileType('rb', 0), help='load from binary file')
|
||||
load_parser_from_grp.add_argument('--from-cap-file', type=argparse.FileType('rb', 0), help='load from JAVA-card CAP file')
|
||||
load_parser.add_argument('--chunk-len', type=auto_uint8, default=None,
|
||||
help='Block size for the LOAD command; default: as large as the current secure channel overhead permits, at most 240')
|
||||
|
||||
@cmd2.with_argparser(load_parser)
|
||||
def do_load(self, opts):
|
||||
"""Perform a GlobalPlatform LOAD command. (We currently only support loading without DAP and
|
||||
without ciphering.)"""
|
||||
if opts.from_hex is not None:
|
||||
ADF_SD.load(self._cmd.lchan.scc, h2b(opts.from_hex), opts.chunk_len)
|
||||
self.load(h2b(opts.from_hex))
|
||||
elif opts.from_file is not None:
|
||||
ADF_SD.load(self._cmd.lchan.scc, opts.from_file.read(), opts.chunk_len)
|
||||
self.load(opts.from_file.read())
|
||||
elif opts.from_cap_file is not None:
|
||||
cap = CapFile(opts.from_cap_file)
|
||||
ADF_SD.load(self._cmd.lchan.scc, cap.get_loadfile(), opts.chunk_len)
|
||||
self.load(cap.get_loadfile())
|
||||
else:
|
||||
raise ValueError('load source not specified!')
|
||||
|
||||
def load(self, contents:bytes, chunk_len:int = 240):
|
||||
# TODO:tune chunk_len based on the overhead of the used SCP?
|
||||
# build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case
|
||||
remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents
|
||||
# transfer this in various chunks to the card
|
||||
total_size = len(remainder)
|
||||
block_nr = 0
|
||||
while len(remainder):
|
||||
block = remainder[:chunk_len]
|
||||
remainder = remainder[chunk_len:]
|
||||
# build LOAD command APDU according to GPC_SPE_034 section 11.6.2 / Table 11-56
|
||||
p1 = 0x00 if len(remainder) else 0x80
|
||||
p2 = block_nr % 256
|
||||
block_nr += 1
|
||||
cmd_hex = "80E8%02x%02x%02x%s00" % (p1, p2, len(block), b2h(block))
|
||||
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_install (and make selectable) now!" % (total_size, block_nr))
|
||||
|
||||
install_cap_parser = argparse.ArgumentParser(usage='%(prog)s FILE [--install-parameters | --install-parameters-*]')
|
||||
install_cap_parser.add_argument('cap_file', type=str, metavar='FILE',
|
||||
help='JAVA-CARD CAP file to install')
|
||||
@@ -1117,8 +881,6 @@ class ADF_SD(CardADF):
|
||||
install_cap_parser_inst_prm_grp.add_argument('--install-parameters-stk',
|
||||
type=is_hexstr, default=None,
|
||||
help='Load Parameters (ETSI TS 102 226, section 8.2.1.3.2.1)')
|
||||
install_cap_parser.add_argument('--chunk-len', type=auto_uint8, default=None,
|
||||
help='Block size for the LOAD command; default: as large as the current secure channel overhead permits, at most 240')
|
||||
|
||||
@cmd2.with_argparser(install_cap_parser)
|
||||
def do_install_cap(self, opts):
|
||||
@@ -1157,7 +919,7 @@ class ADF_SD(CardADF):
|
||||
self._cmd.poutput("step #1: install for load...")
|
||||
self.do_install_for_load("--load-file-aid %s --security-domain-aid %s" % (load_file_aid, security_domain_aid))
|
||||
self._cmd.poutput("step #2: load...")
|
||||
ADF_SD.load(self._cmd.lchan.scc, load_file, opts.chunk_len)
|
||||
self.load(load_file)
|
||||
self._cmd.poutput("step #3: install_for_install (and make selectable)...")
|
||||
self.do_install_for_install("--load-file-aid %s --module-aid %s --application-aid %s --install-parameters %s --make-selectable" %
|
||||
(load_file_aid, module_aid, application_aid, install_parameters))
|
||||
@@ -1197,7 +959,7 @@ class ADF_SD(CardADF):
|
||||
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(8)
|
||||
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
|
||||
scp02 = SCP02(card_keys=kset)
|
||||
ADF_SD.establish_scp(self._cmd.lchan.scc, scp02, host_challenge, opts.security_level)
|
||||
self._establish_scp(scp02, host_challenge, opts.security_level)
|
||||
|
||||
est_scp03_parser = deepcopy(est_scp02_parser)
|
||||
est_scp03_parser.description = None
|
||||
@@ -1225,15 +987,27 @@ class ADF_SD(CardADF):
|
||||
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(s_mode)
|
||||
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
|
||||
scp03 = SCP03(card_keys=kset, s_mode = s_mode)
|
||||
ADF_SD.establish_scp(self._cmd.lchan.scc, scp03, host_challenge, opts.security_level)
|
||||
self._establish_scp(scp03, host_challenge, opts.security_level)
|
||||
|
||||
def _establish_scp(self, scp, host_challenge, security_level):
|
||||
# perform the common functionality shared by SCP02 and SCP03 establishment
|
||||
init_update_apdu = scp.gen_init_update_apdu(host_challenge=host_challenge)
|
||||
init_update_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(init_update_apdu))
|
||||
scp.parse_init_update_resp(h2b(init_update_resp))
|
||||
ext_auth_apdu = scp.gen_ext_auth_apdu(security_level)
|
||||
_ext_auth_resp, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(ext_auth_apdu))
|
||||
self._cmd.poutput("Successfully established a %s secure channel" % str(scp))
|
||||
# store a reference to the SCP instance
|
||||
self._cmd.lchan.scc.scp = scp
|
||||
self._cmd.update_prompt()
|
||||
|
||||
|
||||
def do_release_scp(self, _opts):
|
||||
"""Release a previously establiehed secure channel."""
|
||||
if not self._cmd.lchan.scc.scp:
|
||||
self._cmd.poutput("Cannot release SCP as none is established")
|
||||
return
|
||||
ADF_SD.release_scp(self._cmd.lchan.scc)
|
||||
self._cmd.lchan.scc.scp = None
|
||||
self._cmd.update_prompt()
|
||||
|
||||
|
||||
@@ -1291,16 +1065,10 @@ def compute_kcv_aes(key:bytes) -> bytes:
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
return cipher.encrypt(plaintext)
|
||||
|
||||
def compute_kcv_psk(key:bytes) -> bytes:
|
||||
# GP Amendment B v1.2, 3.9.1 / Table 3-13
|
||||
# KCV of a PSK TLS key is the 3 highest-order bytes of the SHA-1 digest of the clear key value.
|
||||
return hashlib.sha1(key).digest()
|
||||
|
||||
# dict is keyed by the string name of the KeyType enum above in this file
|
||||
KCV_CALCULATOR = {
|
||||
'aes': compute_kcv_aes,
|
||||
'des': compute_kcv_des,
|
||||
'tls_psk': compute_kcv_psk,
|
||||
}
|
||||
|
||||
def compute_kcv(key_type: str, key: bytes) -> Optional[bytes]:
|
||||
|
||||
@@ -182,29 +182,6 @@ class SCP(SecureChannel, abc.ABC):
|
||||
"""Should we perform R-ENC?"""
|
||||
return self.security_level & 0x20
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def mac_len(self) -> int:
|
||||
"""Length of the appended C-MAC, to be provided by derived class."""
|
||||
|
||||
@property
|
||||
def overhead(self) -> int:
|
||||
"""Worst-case len that wrapping a command APDU adds to its data field at the
|
||||
current sec level is (255 - overhead), C-MAC + C-DECRYPTION encryption padding."""
|
||||
if not self.do_cmac:
|
||||
return 0
|
||||
if not self.do_cenc:
|
||||
return self.mac_len
|
||||
# see Secure Channel Protocol '03' Card Specification v2.3 - Amendment D v1.1.2
|
||||
# which defers to GPCS v2.3 Section B.2 which then defers to
|
||||
# NIST SP 800-38B for encryption and points out that
|
||||
# the padding is, as expected, just the usual padding from NIST SP 800-38A
|
||||
# C-DECRYPTION pads with ('80'+['00'...] at least 1 byte) up to
|
||||
# the cipher block size + C-MAC on top -> largest usable data field
|
||||
# is one byte less than the largest block-size multiple within 255 - mac_len.
|
||||
bs = self.sk.blocksize
|
||||
return 255 - ((255 - self.mac_len) // bs * bs - 1)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s[%02x]" % (self.__class__.__name__, self.security_level)
|
||||
|
||||
@@ -238,20 +215,11 @@ class SCP(SecureChannel, abc.ABC):
|
||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
||||
pass
|
||||
|
||||
def pad_to_blocksize(self, data: bytes) -> bytes:
|
||||
"""Right pad the data with zero bytes to a multiple of the DEK cipher block size."""
|
||||
if len(data) % self.sk.blocksize:
|
||||
# not '+=' which would mutate the callers bytearray in place..
|
||||
data = data + b'\x00' * (self.sk.blocksize - len(data) % self.sk.blocksize)
|
||||
return data
|
||||
|
||||
def encrypt_key(self, key: bytes) -> bytes:
|
||||
"""Encrypt a key with the DEK."""
|
||||
if len(key) % self.sk.blocksize:
|
||||
# The kcv is right padded before encryption and the kcb
|
||||
# is formatted as described in Table 11-70: preceded by the actual length of the
|
||||
# clear text kcv.
|
||||
return bertlv_encode_len(len(key)) + self.dek_encrypt(self.pad_to_blocksize(key))
|
||||
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:
|
||||
@@ -264,8 +232,9 @@ class SCP(SecureChannel, abc.ABC):
|
||||
# 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.
|
||||
key_len, remainder = bertlv_parse_len(encrypted_key)
|
||||
return self.dek_decrypt(remainder)[:key_len]
|
||||
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
|
||||
@@ -291,8 +260,10 @@ class SCP02(SCP):
|
||||
# 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]]
|
||||
# C-MAC (Single DES + final 3DES, B.1.2.2) is always one full DES block
|
||||
mac_len = 8
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.overhead = 8
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||
# See also GPC section B.1.1.2, E.4.7, and E.4.1
|
||||
@@ -367,16 +338,10 @@ class SCP02(SCP):
|
||||
# CMAC on modified APDU
|
||||
mlc = lc + 8
|
||||
clac = cla | CLA_SM
|
||||
if mlc >= 256:
|
||||
raise ValueError('Modified Lc (%u) would exceed maximum when appending 8 bytes of mac' % mlc)
|
||||
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + data)
|
||||
if self.do_cenc:
|
||||
padded_data = pad80(data, 8)
|
||||
if len(padded_data) + 8 >= 256:
|
||||
raise ValueError('Modified Lc (%u) would exceed maximum when appending padding and mac' %
|
||||
(len(padded_data) + 8))
|
||||
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
|
||||
data = k.encrypt(padded_data)
|
||||
data = k.encrypt(pad80(data, 8))
|
||||
lc = len(data)
|
||||
|
||||
lc += 8
|
||||
@@ -512,13 +477,9 @@ class SCP03(SCP):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.s_mode = kwargs.pop('s_mode', 8)
|
||||
self.overhead = self.s_mode
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def mac_len(self) -> int:
|
||||
# C-MAC truncated to 8 in S8 or 16 bytes in S16 mode
|
||||
return self.s_mode
|
||||
|
||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||
cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
|
||||
return cipher.encrypt(plaintext)
|
||||
|
||||
+1
-2
@@ -24,7 +24,6 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
import enum
|
||||
import cmd2
|
||||
from packaging import version
|
||||
|
||||
@@ -127,7 +126,7 @@ class PySimLogger:
|
||||
formatted_message = logging.Formatter.format(PySimLogger.__formatter, record)
|
||||
color = PySimLogger.colors.get(record.levelno)
|
||||
if color:
|
||||
if isinstance(color, str) and not isinstance(color, enum.Enum):
|
||||
if isinstance(color, str):
|
||||
PySimLogger.print_callback(color + formatted_message + "\033[0m")
|
||||
else:
|
||||
PySimLogger.print_callback(_style(formatted_message, fg = color))
|
||||
|
||||
+10
-247
@@ -18,12 +18,10 @@
|
||||
import zlib
|
||||
import abc
|
||||
import struct
|
||||
from typing import Optional, Tuple, List, Union
|
||||
from construct import ConstructError, Enum, Int8ub, Int16ub, Struct, BitsInteger, BitStruct
|
||||
from typing import Optional, Tuple
|
||||
from construct import Enum, Int8ub, Int16ub, Struct, BitsInteger, BitStruct
|
||||
from construct import Flag, Padding, Switch, this, PrefixedArray, GreedyRange
|
||||
from construct import Const, Prefixed, Select, Construct, SizeofError, stream_read, stream_write
|
||||
from osmocom.construct import *
|
||||
from osmocom.tlv import bertlv_encode_len
|
||||
from osmocom.utils import b2h
|
||||
|
||||
from pySim.sms import UserDataHeader
|
||||
@@ -58,217 +56,6 @@ CompactRemoteResp = Struct('number_of_commands'/Int8ub,
|
||||
'last_status_word'/HexAdapter(Bytes(2)),
|
||||
'last_response_data'/HexAdapter(GreedyBytes))
|
||||
|
||||
######################################################################
|
||||
# Expanded Remote Application data format, ETSI TS 102 226 V19.0.0 (2025-11) Section 5.2
|
||||
# 5.2.1 Expanded Remote command structure
|
||||
# 5.2.1.1 C-APDU TLV
|
||||
# 5.2.1.2 Immediate Action TLV
|
||||
# 5.2.1.3 Error Action TLV
|
||||
# 5.2.1.4 Script Chaining TLV
|
||||
# 5.2.2 Expanded Remote response structure (tables 5.10 .. 5.16)
|
||||
#
|
||||
# definite length coding and indefinite length coding are supported.
|
||||
#
|
||||
# BER-TLV tag values from ETSI TS 101 220 V19.0.0 tables 7.18, 7.19, 7.20
|
||||
# C-APDU / R-APDU ETSI TS 102 223 Section 8.35 + 8.36
|
||||
# inside these the CR flag of the tag is 0 (TS 101 220 tables 7.19/7.20),
|
||||
# so tag bytes are 22 and 23 and not A2/A3.
|
||||
#
|
||||
# This layer sits above the TS 102 225 security layer.
|
||||
######################################################################
|
||||
|
||||
class BerTlvLength(Construct):
|
||||
"""A definite-length BER-TLV length field used by the "expanded remote
|
||||
application data format" from ISO/IEC 8825-1 referenced by TS 102 226 5.2
|
||||
|
||||
- short form (0..127 -> single octet)
|
||||
- long form (128.. -> 0x8N followed by N length octets)
|
||||
Indefinite length coding (first octet 0x80, TS 102 226 tables 5.2a/5.10a)
|
||||
is omitted here because it is only recommended for HTTPS/CoAP transport, not SMS."""
|
||||
def _parse(self, stream, context, path):
|
||||
first = stream_read(stream, 1, path)[0]
|
||||
if first < 0x80:
|
||||
return first
|
||||
num_octets = first & 0x7f
|
||||
if num_octets == 0:
|
||||
raise NotImplementedError('indefinite coding is not supported')
|
||||
return int.from_bytes(stream_read(stream, num_octets, path), 'big')
|
||||
|
||||
def _build(self, obj, stream, context, path):
|
||||
encoded = bertlv_encode_len(obj)
|
||||
stream_write(stream, encoded, len(encoded), path)
|
||||
return obj
|
||||
|
||||
def _sizeof(self, context, path):
|
||||
raise SizeofError('BER-TLV length has a variable size?!')
|
||||
|
||||
BerTlvLen = BerTlvLength()
|
||||
|
||||
class _RApduValueAdapter(Adapter):
|
||||
"""Split/join value of R-APDU COMPREHENSION-TLV TS 102 223 8.36
|
||||
[R-APDU data (x-2 bytes)] SW1 SW2."""
|
||||
def _decode(self, obj, context, path):
|
||||
raw = bytes(obj)
|
||||
return Container(response_data=b2h(raw[:-2]), status_word=b2h(raw[-2:]))
|
||||
|
||||
def _encode(self, obj, context, path):
|
||||
return h2b(obj['response_data']) + h2b(obj['status_word'])
|
||||
|
||||
#### Command Scripting template TS 102 226 tables 5.2 / 5.2a, TS 101 220 tables 7.18/7.19
|
||||
#
|
||||
# The two TS 101 220 table 7.18 length codings use different template tags:
|
||||
# - definite tag AA
|
||||
# - indefinite AE
|
||||
# In both codings the inner Command TLVs use definite length coding, only the
|
||||
# surrounding template differs.
|
||||
|
||||
# TS 102 223 8.35
|
||||
ExpandedC_APDU = Struct('_tag'/Const(b'\x22'),
|
||||
'c_apdu'/Prefixed(BerTlvLen, HexAdapter(GreedyBytes)))
|
||||
|
||||
# shared by both length codings.
|
||||
ExpandedCmdItems = GreedyRange(ExpandedC_APDU)
|
||||
|
||||
# TS 102 226 table 5.2: Command Scripting template, definite length coding only
|
||||
ExpandedCmd = Struct('_tag'/Const(b'\xaa'),
|
||||
'commands'/Prefixed(BerTlvLen, ExpandedCmdItems))
|
||||
|
||||
# TS 102 226 table 5.2a: indefinite length coding, 'AE 80 <C-APDU TLVs> 00 00'. GreedyRange
|
||||
# stops at the first octet that is not a C-APDU tag, which is the end-of-contents marker.
|
||||
ExpandedCmdIndef = Struct('_tag'/Const(b'\xae'), '_indef'/Const(b'\x80'),
|
||||
'commands'/ExpandedCmdItems, '_eoc'/Const(b'\x00\x00'))
|
||||
|
||||
#### Response Scripting template TS 102 226 5.2.2, tables 5.10-5.16, TS 101 220 table 7.20
|
||||
|
||||
# TS 102 223 8.36
|
||||
ExpandedR_APDU = Struct('_tag'/Const(b'\x23'),
|
||||
'r_apdu'/Prefixed(BerTlvLen, _RApduValueAdapter(GreedyBytes)))
|
||||
|
||||
# TS 102 226 table 5.11
|
||||
# Value is an integer per ISO/IEC 8825-1, likely just one octet.
|
||||
ExpandedNumExecuted = Struct('_tag'/Const(b'\x80'),
|
||||
'number_of_commands'/Prefixed(BerTlvLen, GreedyInteger()))
|
||||
|
||||
# TS 102 226 table 5.12
|
||||
ExpandedBadFormat = Struct('_tag'/Const(b'\x90'),
|
||||
'bad_format'/Prefixed(BerTlvLen,
|
||||
Enum(Int8ub, unknown_tag=1, wrong_length=2, length_not_found=3)))
|
||||
|
||||
# TS 102 226 table 5.14
|
||||
ExpandedImmediateActionResp = Struct('_tag'/Const(b'\x81'),
|
||||
'immediate_action_response'/Prefixed(BerTlvLen,
|
||||
Enum(Int8ub, suspension_error=1)))
|
||||
|
||||
# TS 102 226 table 5.16
|
||||
ExpandedScriptChainingResp = Struct('_tag'/Const(b'\x83'),
|
||||
'script_chaining_response'/Prefixed(BerTlvLen,
|
||||
Enum(Int8ub, no_previous_script=1,
|
||||
not_supported=2, unable_to_process=3)))
|
||||
|
||||
# response TLVs shared by the def and indef Response Scripting templates
|
||||
ExpandedRespItems = GreedyRange(Select(ExpandedR_APDU,
|
||||
ExpandedBadFormat,
|
||||
ExpandedImmediateActionResp,
|
||||
ExpandedScriptChainingResp))
|
||||
|
||||
# - starts with the "Number of executed command TLV objects" (table 5.10/5.13/5.15)
|
||||
# - followed by a sequence of R-APDU TLVs
|
||||
# - and/or one of the error # response TLVs
|
||||
ExpandedRemoteResp = Struct('_tag'/Const(b'\xab'),
|
||||
'body'/Prefixed(BerTlvLen, Struct(
|
||||
'num_executed'/ExpandedNumExecuted,
|
||||
'responses'/ExpandedRespItems)))
|
||||
|
||||
# TS 102 226 table 5.10a: indefinite length coding, no "number of executed" TLV
|
||||
ExpandedRemoteRespIndef = Struct('_tag'/Const(b'\xaf'), '_indef'/Const(b'\x80'),
|
||||
'responses'/ExpandedRespItems, '_eoc'/Const(b'\x00\x00'))
|
||||
|
||||
|
||||
def encode_expanded_cmd(apdus: Union[bytes, List[bytes]],
|
||||
length_coding: str = 'definite') -> bytes:
|
||||
"""builds the Command Scripting template, TS 102 226 5.2.1
|
||||
|
||||
Args:
|
||||
apdus: single C-APDU bytes or list of C-APDUs bytes. Each
|
||||
C-APDU is wrapped into a C-APDU TLV- This function does not add
|
||||
or modify Le.
|
||||
length_coding: 'definite' (the default, tag 'AA', table 5.2) or
|
||||
'indefinite' (tag 'AE', table 5.2a: 'AE 80 <cmd TLVs> 00 00').
|
||||
Inner C-APDU TLVs use definite length coding in both cases.
|
||||
Returns:
|
||||
encoded Command Scripting template as bytes
|
||||
"""
|
||||
if isinstance(apdus, (bytes, bytearray)):
|
||||
apdus = [apdus]
|
||||
commands = [{'c_apdu': b2h(a)} for a in apdus]
|
||||
if length_coding == 'definite':
|
||||
return ExpandedCmd.build({'commands': commands})
|
||||
if length_coding == 'indefinite':
|
||||
return ExpandedCmdIndef.build({'commands': commands})
|
||||
raise ValueError("Invalid length_coding: %r" % length_coding)
|
||||
|
||||
|
||||
def decode_expanded_resp(data: bytes) -> Container:
|
||||
"""Decode a Response Scripting template, TS 102 226 5.2.2 def and indef length
|
||||
coding
|
||||
|
||||
returned Container has:
|
||||
number_of_commands -- "number of executed command TLV objects" table 5.11
|
||||
for definite coding. indefinite coding does not have
|
||||
this TLV, so report the number of returned R-APDUs instead.
|
||||
commands -- list of Containers, one per R-APDU TLV, each
|
||||
with 'response_data' and 'status_word' hexstr
|
||||
last_response_data -- response_data of the last R-APDU or ''
|
||||
last_status_word -- status_word of the last R-APDU or None
|
||||
truncated -- True if any R-APDU has SW 62F1.
|
||||
5.2.1.1 states card sets that status when it had to truncate
|
||||
C-APDU response data, and "this shall terminate the
|
||||
processing of the command list".
|
||||
so the response is short AND the remaining commands never ran.
|
||||
bad_format -- error type of a trailing Bad format TLV if present
|
||||
immediate_action_response -- Immediate Action Response TLV, if there was a suspension error
|
||||
script_chaining_response -- Script Chaining Response TLV, if there was a chaining error
|
||||
|
||||
The 'last_response_data'/'last_status_word'/'number_of_commands' keys are compatible with
|
||||
CompactRemoteResp so existing callers keep working."""
|
||||
if isinstance(data, str):
|
||||
data = h2b(data)
|
||||
try:
|
||||
if data[:1] == b'\xaf':
|
||||
responses = ExpandedRemoteRespIndef.parse(data)['responses']
|
||||
num_executed = None
|
||||
else:
|
||||
parsed = ExpandedRemoteResp.parse(data)
|
||||
responses = parsed['body']['responses']
|
||||
num_executed = parsed['body']['num_executed']['number_of_commands']
|
||||
except ConstructError as e:
|
||||
raise ValueError('malformed Response Scripting template: %s' % e) from e
|
||||
|
||||
commands = []
|
||||
bad_format = None
|
||||
immediate_action_response = None
|
||||
script_chaining_response = None
|
||||
for item in responses:
|
||||
if 'r_apdu' in item:
|
||||
commands.append(Container(response_data=item['r_apdu']['response_data'],
|
||||
status_word=item['r_apdu']['status_word']))
|
||||
elif 'bad_format' in item:
|
||||
bad_format = item['bad_format']
|
||||
elif 'immediate_action_response' in item:
|
||||
immediate_action_response = item['immediate_action_response']
|
||||
elif 'script_chaining_response' in item:
|
||||
script_chaining_response = item['script_chaining_response']
|
||||
# TS 102 226 5.2.1.1: 62F1 means response of a C-APDU was truncated, processing terminated
|
||||
truncated = any(c['status_word'].lower() == '62f1' for c in commands)
|
||||
return Container(number_of_commands=num_executed if num_executed is not None else len(commands),
|
||||
commands=commands,
|
||||
last_response_data=commands[-1]['response_data'] if commands else '',
|
||||
last_status_word=commands[-1]['status_word'] if commands else None,
|
||||
truncated=truncated,
|
||||
bad_format=bad_format,
|
||||
immediate_action_response=immediate_action_response,
|
||||
script_chaining_response=script_chaining_response)
|
||||
|
||||
RC_CC_DS = Enum(BitsInteger(2), no_rc_cc_ds=0, rc=1, cc=2, ds=3)
|
||||
CNTR_REQ = Enum(BitsInteger(2), no_counter=0, counter_no_replay_or_seq=1, counter_must_be_higher=2, counter_must_be_lower=3)
|
||||
POR_REQ = Enum(BitsInteger(2), no_por=0, por_required=1, por_only_when_error=2)
|
||||
@@ -362,23 +149,13 @@ class OtaDialect(abc.ABC):
|
||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||
|
||||
@abc.abstractmethod
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict,
|
||||
apdu: Union[bytes, List[bytes]], remote_format: str = 'compact') -> bytes:
|
||||
"""Encode a command for a format.
|
||||
|
||||
remote_format:
|
||||
'compact' TS 102 226 5.1, DEFAULT assumes apdus are opaque already-concatenated command strings
|
||||
'expanded' TS 102 226 5.2 wraps a single C-APDU or list of C-APDUs in a Command Scripting template."""
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes,
|
||||
remote_format: str = 'compact') -> (object, Optional[object]):
|
||||
"""Decode response into response packet + a decoded response if por_ok.
|
||||
|
||||
remote_format:
|
||||
'compact' -> DEFAULT TS 102 226 5.1.2 CompactRemoteResp2
|
||||
'expanded' -> container returned by decode_expanded_resp(), TS 102 226 5.2.2
|
||||
def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
|
||||
"""Decode a response into a response packet and, if indicted (by a
|
||||
response status of `"por_ok"`) a decoded response.
|
||||
|
||||
The response packet's common characteristics are not fully determined,
|
||||
and (so far) completely proprietary per dialect."""
|
||||
@@ -558,16 +335,7 @@ class OtaDialectSms(OtaDialect):
|
||||
'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: Union[bytes, List[bytes]], remote_format: str = 'compact') -> bytes:
|
||||
# as above:
|
||||
# expanded format is a Command Scripting template wrapping the C-APDU(s)
|
||||
# compact format passes already concatenated command string
|
||||
if remote_format == 'expanded':
|
||||
apdu = encode_expanded_cmd(apdu)
|
||||
elif remote_format != 'compact':
|
||||
raise ValueError("Invalid remote_format: %s" % remote_format)
|
||||
|
||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
||||
# length of signature in octets
|
||||
len_sig = self._compute_sig_len(spi)
|
||||
pad_cnt = 0
|
||||
@@ -678,10 +446,7 @@ class OtaDialectSms(OtaDialect):
|
||||
return hdr_dec['tar'], spi, apdu
|
||||
|
||||
|
||||
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes,
|
||||
remote_format: str = 'compact') -> ("OtaDialectSms.SmsResponsePacket", Optional[object]):
|
||||
if remote_format not in ('compact', 'expanded'):
|
||||
raise ValueError("Invalid remote_format: %s ?!" % remote_format)
|
||||
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes) -> ("OtaDialectSms.SmsResponsePacket", Optional["CompactRemoteResp"]):
|
||||
if isinstance(data, str):
|
||||
data = h2b(data)
|
||||
# plain-text POR: 027100000e0ab000110000000000000001612f
|
||||
@@ -727,11 +492,9 @@ class OtaDialectSms(OtaDialect):
|
||||
else:
|
||||
raise OtaCheckError('Unknown por_rc_cc_ds: %s' % spi['por_rc_cc_ds'])
|
||||
|
||||
# TODO: ExpandedRemoteResponse according to TS 102 226 5.2.2
|
||||
if res.response_status == 'por_ok' and len(res['secured_data']):
|
||||
if remote_format == 'expanded':
|
||||
dec = decode_expanded_resp(res['secured_data'])
|
||||
else:
|
||||
dec = CompactRemoteResp.parse(res['secured_data'])
|
||||
dec = CompactRemoteResp.parse(res['secured_data'])
|
||||
else:
|
||||
dec = None
|
||||
return (res, dec)
|
||||
|
||||
+3
-109
@@ -19,7 +19,6 @@
|
||||
|
||||
import typing
|
||||
import abc
|
||||
import logging
|
||||
from bidict import bidict
|
||||
from construct import Int8ub, Byte, Bit, Flag, BitsInteger
|
||||
from construct import Struct, Enum, Tell, BitStruct, this, Padding
|
||||
@@ -29,8 +28,6 @@ from osmocom.utils import Hexstr, h2b, b2h
|
||||
|
||||
from smpp.pdu import pdu_types, operations
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BytesOrHex = typing.Union[Hexstr, bytes]
|
||||
|
||||
class UserDataHeader:
|
||||
@@ -63,109 +60,6 @@ class UserDataHeader:
|
||||
return self._construct.build({'ies':self.ies, 'data':b''})
|
||||
|
||||
|
||||
class ConcatenatedSmsReassembler:
|
||||
"""3GPP TS 23.040 section 9.2.3.24 concat multi part reassembly
|
||||
|
||||
A large user-data payload (e.g. a big OTA response packet) is split by the
|
||||
sending entity into several SMS,
|
||||
each carries a
|
||||
- "concat short messages" IE in its UDH that identifies the set (ref num),
|
||||
- total number of parts
|
||||
- this parts seqno.
|
||||
supports both:
|
||||
IEI 0x00, section 9.2.3.24.1 8-bit ref form
|
||||
IEI 0x08, section 9.2.3.24.8 the 16-bit ref form
|
||||
|
||||
Feed each received TP-User-Data (UDH + payload) to add() which
|
||||
returns the reassembled TP-User-Data once all parts of the set have arrived,
|
||||
or None as long as parts are still missing.
|
||||
|
||||
A non-concatenated SMS is returned unchanged,
|
||||
just like one where the concat IE holds a reserved value:
|
||||
TS 23.040 9.2.3.24.1 says
|
||||
- both a total of zero
|
||||
- a sequence number that is zero or greater than the total
|
||||
that "the receiving entity shall ignore the whole IE",
|
||||
we treat the message as a single, non-concatenated one and warn, not
|
||||
as an error, so the caller does not die.
|
||||
|
||||
The reassembled TP-User-Data is built with a UDH that contains
|
||||
the non-concat IEs seen in the parts, for example the the OTA "response packet"
|
||||
indicator IE 0x71, followed by the concatenated payloads in sequence order,
|
||||
so exactly the single-SMS form the sender would have produced for a payload that fits
|
||||
into one SMS.
|
||||
This allows convenient decoding by the normal single part path."""
|
||||
|
||||
# IEI: Concatenated short messages, 8-bit reference number
|
||||
# (see 3GPP TS 23.040 section 9.2.3.24 and section 9.2.3.24.1)
|
||||
CONCAT_8BIT = 0x00
|
||||
# IEI: Concatenated short message, 16-bit reference number
|
||||
# (see 3GPP TS 23.040 section 9.2.3.24 and section 9.2.3.24.8)
|
||||
CONCAT_16BIT = 0x08
|
||||
|
||||
def __init__(self, max_sets: int = 8):
|
||||
# keyed by (iei, ref, total): {'parts': {seq: payload}, 'header_ies'}, insertion ordered
|
||||
self.sets = {}
|
||||
self.max_sets = max_sets # incomplete sets kept, oldest is dropped beyond that
|
||||
|
||||
@classmethod
|
||||
def _parse_concat_ie(cls, ies) -> typing.Optional[typing.Tuple[int, int, int, int]]:
|
||||
"""Return (iei, ref, total, seq) of the concat IE, or None"""
|
||||
for ie in ies:
|
||||
if ie['iei'] == cls.CONCAT_8BIT and ie['length'] == 3:
|
||||
v = ie['value']
|
||||
return cls.CONCAT_8BIT, v[0], v[1], v[2]
|
||||
if ie['iei'] == cls.CONCAT_16BIT and ie['length'] == 4:
|
||||
v = ie['value']
|
||||
return cls.CONCAT_16BIT, int.from_bytes(v[0:2], 'big'), v[2], v[3]
|
||||
return None
|
||||
|
||||
def add(self, tpud: BytesOrHex) -> typing.Optional[bytes]:
|
||||
"""Add one TP-User-Data.
|
||||
Returns
|
||||
- the reassembled TP-User-Data if set is complete or sms not multipart,
|
||||
- else None"""
|
||||
if isinstance(tpud, str):
|
||||
tpud = h2b(tpud)
|
||||
udh, payload = UserDataHeader.from_bytes(tpud)
|
||||
concat = self._parse_concat_ie(udh.ies)
|
||||
if concat is None:
|
||||
return tpud
|
||||
iei, ref, total, seq = concat
|
||||
if total < 1 or seq < 1 or seq > total:
|
||||
# TS 23.040 9.2.3.24.1 / 9.2.3.24.8, total zero or seqno zero / > total:
|
||||
# Ignoring the IE means the message has no valid concat IE, which is a single part message.
|
||||
# Better warn and hand it back rather than raise, so we don't kill the callers receive loop/session
|
||||
logger.warning('Ignoring reserved concat IE (ref=%u total=%u seq=%u), treating the '
|
||||
'message as non-concat', ref, total, seq)
|
||||
return tpud
|
||||
# TS 23.040 9.2.3.24.1 Total is constant in a set, refno only unique per IE form -> both set identity
|
||||
# - full count = seqno 1..total is present
|
||||
# - part disagreeing on the total ends up as set that cannot complete like set with missing parts
|
||||
key = (iei, ref, total)
|
||||
if key not in self.sets and len(self.sets) >= self.max_sets:
|
||||
del self.sets[next(iter(self.sets))]
|
||||
s = self.sets.setdefault(key, {'parts': {}, 'header_ies': []})
|
||||
s['parts'][seq] = payload
|
||||
# - remember the non concat IEs (OTA 0x71 indicator for example)
|
||||
# - keep first seen occurrence of each IEI,
|
||||
# so app IE present only in the first segment is preserved independent of arrival order
|
||||
seen = {ie['iei'] for ie in s['header_ies']}
|
||||
for ie in udh.ies:
|
||||
if ie['iei'] in (self.CONCAT_8BIT, self.CONCAT_16BIT):
|
||||
continue
|
||||
if ie['iei'] not in seen:
|
||||
s['header_ies'].append(ie)
|
||||
seen.add(ie['iei'])
|
||||
if len(s['parts']) < total:
|
||||
return None
|
||||
# all parts present -> reassemble in seq order
|
||||
del self.sets[(iei, ref, total)]
|
||||
body = b''.join(s['parts'][i] for i in range(1, total + 1))
|
||||
header = UserDataHeader(s['header_ies']).to_bytes()
|
||||
return header + body
|
||||
|
||||
|
||||
def smpp_dcs_is_8bit(dcs: pdu_types.DataCoding) -> bool:
|
||||
"""Determine if the given SMPP data coding scheme is 8-bit or not."""
|
||||
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
||||
@@ -246,8 +140,8 @@ class AddressField:
|
||||
def to_bytes(self) -> bytes:
|
||||
"""Encode the AddressField into the binary representation as used in T-PDU."""
|
||||
num_digits = len(self.digits)
|
||||
# don't store the filler nibble or get_bytes() encodes it as digit and ends up too large
|
||||
digits = self.digits + 'f' if num_digits % 2 else self.digits
|
||||
if num_digits % 2:
|
||||
self.digits += 'f'
|
||||
d = {
|
||||
'addr_len': num_digits,
|
||||
'type_of_addr': {
|
||||
@@ -255,7 +149,7 @@ class AddressField:
|
||||
'type_of_number': self.ton,
|
||||
'numbering_plan_id': self.npi,
|
||||
},
|
||||
'digits': digits,
|
||||
'digits': self.digits,
|
||||
}
|
||||
return self._construct.build(d)
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# coding=utf-8
|
||||
"""Utilities / Functions related to sysmocom sysmoUSIM-SJS1 cards
|
||||
|
||||
(C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
All Rights Reserved
|
||||
|
||||
Author: Eric Wild <ewild@sysmocom.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from construct import Struct, Bytes, Flag
|
||||
from osmocom.utils import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.filesystem import *
|
||||
from pySim.runtime import RuntimeState
|
||||
|
||||
|
||||
class EF_Ki(TransparentEF):
|
||||
_test_de_encode = [
|
||||
('000102030405060708090a0b0c0d0e0f',
|
||||
{'key': h2b('000102030405060708090a0b0c0d0e0f')}),
|
||||
]
|
||||
|
||||
def __init__(self, fid='00ff', name='EF.Ki'):
|
||||
super().__init__(fid, name=name, desc='K/Ki authentication key', size=(16, 16))
|
||||
self._construct = Struct('key'/Bytes(16))
|
||||
|
||||
|
||||
class EF_OPc(TransparentEF):
|
||||
_test_de_encode = [
|
||||
('016ca53d7a0a804561646816d7b0c702fb',
|
||||
{'use_opc_instead_of_op': True, 'op_opc': h2b('6ca53d7a0a804561646816d7b0c702fb')}),
|
||||
]
|
||||
|
||||
def __init__(self, fid='00f7', name='EF.OPc'):
|
||||
super().__init__(fid, name=name, desc='OP/OPc for milenage', size=(17, 17))
|
||||
self._construct = Struct('use_opc_instead_of_op'/Flag, 'op_opc'/Bytes(16))
|
||||
|
||||
|
||||
class SysmoUSIMSJS1(CardModel):
|
||||
_atrs = ["3b9f96801fc78031a073be21136743200718000001a5"]
|
||||
|
||||
@classmethod
|
||||
def add_files(cls, rs: RuntimeState):
|
||||
"""Add sysmoUSIM-SJS1 specific files to given RuntimeState."""
|
||||
# the key material lives in DF.GSM shared with ADF.USIM
|
||||
if '7f20' in rs.mf.children:
|
||||
rs.mf.children['7f20'].add_files([EF_Ki(), EF_OPc()])
|
||||
@@ -45,10 +45,8 @@ class ApduTracer:
|
||||
|
||||
class StdoutApduTracer(ApduTracer):
|
||||
"""Minimalistic APDU tracer, printing commands to stdout."""
|
||||
def trace_command(self, cmd):
|
||||
log.info("-> %s %s", cmd[:10], cmd[10:])
|
||||
|
||||
def trace_response(self, cmd, sw, resp):
|
||||
log.info("-> %s %s", cmd[:10], cmd[10:])
|
||||
log.info("<- %s: %s", sw, resp)
|
||||
|
||||
def trace_reset(self):
|
||||
@@ -72,26 +70,10 @@ class ProactiveHandler(abc.ABC):
|
||||
raise NotImplementedError('No handler method for %s' % pcmd.decoded)
|
||||
|
||||
def prepare_response(self, pcmd: ProactiveCommand, general_result: str = 'performed_successfully'):
|
||||
# TERMINAL RESPONSE per ETSI TS 102 223 section 6.8: Command details (6.8.1) echoed from the
|
||||
# command, Device identities (6.8.2) with source and destination swapped, Result (6.8.3).
|
||||
# pcmd can be
|
||||
# - decoded proactive command IE (.children contains CommandDetails/DeviceIdentities)
|
||||
# - ProactiveCommand collection wrapper (empty .children).
|
||||
# Normalise to the children obj, so both work:
|
||||
# - handler that passes its decoded command
|
||||
# - fallback path that passes collection
|
||||
children = list(getattr(pcmd, 'children', None) or [])
|
||||
if not any(isinstance(c, CommandDetails) for c in children):
|
||||
decoded = getattr(pcmd, 'decoded', None)
|
||||
if decoded is not None and decoded is not pcmd:
|
||||
children = list(getattr(decoded, 'children', None) or [])
|
||||
# The Command Details are echoed from the command that has been processed.
|
||||
command_details = next((c for c in children if isinstance(c, CommandDetails)), None)
|
||||
(command_details,) = [c for c in pcmd.children if isinstance(c, CommandDetails)]
|
||||
# invert the device identities
|
||||
command_dev_ids = next((c for c in children if isinstance(c, DeviceIdentities)), None)
|
||||
if command_details is None or command_dev_ids is None:
|
||||
raise ValueError('failed to prepare TERMINAL RESPONSE: proactive command has no '
|
||||
'CommandDetails/DeviceIdentities (%r)' % (pcmd,))
|
||||
(command_dev_ids,) = [c for c in pcmd.children if isinstance(c, DeviceIdentities)]
|
||||
rsp_dev_ids = DeviceIdentities()
|
||||
rsp_dev_ids.from_dict({'device_identities': {
|
||||
'dest_dev_id': command_dev_ids.decoded['source_dev_id'],
|
||||
@@ -335,18 +317,6 @@ class LinkBaseTpdu(LinkBase):
|
||||
# correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with
|
||||
# legacy scripts that use raw APDU strings, we will still loosely apply GET RESPONSE based on what
|
||||
# the status word indicates. Unless the user explicitly enables the strict mode (set apdu_strict true)
|
||||
#
|
||||
# The dummy GET RESPONSE of clause 4b (see below) is one shot: it turns a warning SW into the 61xx
|
||||
# that announces the response length. It is only ever a valid reaction to the SW returned for the
|
||||
# _command_ TPDU. Once a response has been fetched there is nothing left to announce, so a warning
|
||||
# SW is the final result of the command and has to be passed on to the caller unmodified.
|
||||
#
|
||||
# This matters because the 62xx/63xx range is not exclusive to ETSI TS 102 221.
|
||||
# GPC v2.3.1 section 11.4.3.2 table 11-38 GP GET STATUS (80 F2) answers
|
||||
# 6310 "more data available", meaning "reissue with P2 bit 1 set" as per section 11.4.2.2 table 11-34
|
||||
# rather than "response data is waiting". Trying a random GET RESPONSE at that point
|
||||
# makes the card answer 6982 and tears down the whole SCP session and following commands fail with 6985.
|
||||
dummy_gr_allowed = not data
|
||||
while True:
|
||||
if sw in ['9000', '9100']:
|
||||
# A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command)
|
||||
@@ -359,7 +329,7 @@ class LinkBaseTpdu(LinkBase):
|
||||
# word. (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4a and 3GPP TS 51.011 9.4.1 and
|
||||
# ISO/IEC 7816-4, Table 5)
|
||||
le_gr = sw[2:4]
|
||||
elif sw[0:2] in ['62', '63'] and dummy_gr_allowed:
|
||||
elif sw[0:2] in ['62', '63']:
|
||||
# There are corner cases (status word is 62xx or 63xx) where the UICC/eUICC/SIM asks us
|
||||
# to send a dummy GET RESPONSE command. We send a GET RESPONSE command with a length of 0.
|
||||
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4b and ETSI TS 151 011, section 9.4.1)
|
||||
@@ -374,7 +344,6 @@ class LinkBaseTpdu(LinkBase):
|
||||
data_gr, sw = self.send_tpdu(tpdu_gr)
|
||||
log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)")
|
||||
data += data_gr
|
||||
dummy_gr_allowed = False
|
||||
if sw[0:2] == '6c':
|
||||
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
|
||||
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
|
||||
|
||||
+11
-36
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from bidict import bidict
|
||||
import copy
|
||||
|
||||
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
|
||||
from construct import Optional as COptional, Computed
|
||||
@@ -336,8 +335,6 @@ class TerminalCapability(BER_TLV_IE, tag=0xa9, nested=[TerminalPowerSupply, Exte
|
||||
|
||||
# ETSI TS 102 221 Section 9.2.7 + ISO7816-4 9.3.3/9.3.4
|
||||
class _AM_DO_DF(DataObject):
|
||||
"""ISO7816-4:2005 5.4.3.1 Table 16"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('access_mode', 'Access Mode', tag=0x80)
|
||||
|
||||
@@ -384,7 +381,7 @@ class _AM_DO_DF(DataObject):
|
||||
|
||||
|
||||
class _AM_DO_EF(DataObject):
|
||||
"""ISO7816-4:2005 5.4.3.1 Table 17"""
|
||||
"""ISO7816-4 9.3.2 Table 18 + 9.3.3.1 Table 31"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('access_mode', 'Access Mode', tag=0x80)
|
||||
@@ -432,7 +429,7 @@ class _AM_DO_EF(DataObject):
|
||||
|
||||
|
||||
class _AM_DO_CHDR(DataObject):
|
||||
"""Command Header Access Mode DO according to ISO 7816-4:2005 5.4.3.2 Table 22."""
|
||||
"""Command Header Access Mode DO according to ISO 7816-4 Table 32."""
|
||||
|
||||
def __init__(self, tag):
|
||||
super().__init__('command_header', 'Command Header Description', tag=tag)
|
||||
@@ -546,9 +543,8 @@ class CRT_DO(DataObject):
|
||||
pin = pin_names.inverse[self.decoded]
|
||||
return b'\x83\x01' + pin.to_bytes(1, 'big') + b'\x95\x01\x08'
|
||||
|
||||
# ISO7816-4 9.3.3 Table 33
|
||||
class SecCondByte_DO(DataObject):
|
||||
"""ISO7816-4:2005 5.4.3.1 Table 20"""
|
||||
|
||||
def __init__(self, tag=0x9d):
|
||||
super().__init__('security_condition_byte', tag=tag)
|
||||
|
||||
@@ -736,57 +732,36 @@ class EF_ARR(LinFixedEF):
|
||||
raise ValueError
|
||||
return by_mode
|
||||
|
||||
@staticmethod
|
||||
def __get_do_sequence(decode_for_df : bool = False):
|
||||
if decode_for_df:
|
||||
return DataObjectSequence('arr', sequence=[AM_DO_DF, SC_DO])
|
||||
else:
|
||||
return DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
|
||||
def _decode_record_bin(self, raw_bin_data, **kwargs):
|
||||
# we can only guess if we should decode for EF or DF here, but our caller may
|
||||
# be able to pass us a hint:
|
||||
arr_seq = self.__get_do_sequence(kwargs.get('decode_for_df', False))
|
||||
# we can only guess if we should decode for EF or DF here :(
|
||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
dec = arr_seq.decode_multi(raw_bin_data)
|
||||
# we cannot pass the result through flatten() here, as we don't have a related
|
||||
# 'un-flattening' decoder, and hence would be unable to encode :(
|
||||
return dec[0]
|
||||
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
# we can only guess if we should decode for EF or DF here, but our caller may
|
||||
# be able to pass us a hint:
|
||||
arr_seq = self.__get_do_sequence(kwargs.get('encode_for_df', False))
|
||||
# we can only guess if we should decode for EF or DF here :(
|
||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
return arr_seq.encode_multi(in_json)
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
read_arr_argparser = copy.deepcopy(LinFixedEF.ShellCommands.read_rec_dec_parser)
|
||||
read_arr_argparser.add_argument('--decode-for-df', action='store_true',
|
||||
help='Decode EF.ARR record as if used by a DF (default: EF)')
|
||||
|
||||
@cmd2.with_argparser(read_arr_argparser)
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
|
||||
def do_read_arr_record(self, opts):
|
||||
"""Read one EF.ARR record in flattened, human-friendly form."""
|
||||
(hexdata, _sw) = self._cmd.lchan.read_record(opts.RECORD_NR)
|
||||
data = self._cmd.lchan.selected_file._decode_record_bin(h2b(hexdata),
|
||||
decode_for_df = opts.decode_for_df)
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
read_arrs_argparser = copy.deepcopy(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
||||
read_arrs_argparser.add_argument('--decode-for-df', action='store_true',
|
||||
help='Decode EF.ARR records as if used by a DF (default: EF)')
|
||||
|
||||
@cmd2.with_argparser(read_arrs_argparser)
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
||||
def do_read_arr_records(self, opts):
|
||||
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
# collect all results in list so they are rendered as JSON list when printing
|
||||
data_list = []
|
||||
for recnr in range(1, 1 + num_of_rec):
|
||||
(hexdata, _sw) = self._cmd.lchan.read_record(recnr)
|
||||
data = self._cmd.lchan.selected_file._decode_record_bin(h2b(hexdata),
|
||||
decode_for_df = opts.decode_for_df)
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(recnr)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
data_list.append(data)
|
||||
self._cmd.poutput_json(data_list, opts.oneline)
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
|
||||
#
|
||||
# Author: Eric Wild
|
||||
#
|
||||
# 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
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from osmocom.utils import b2h, h2b
|
||||
|
||||
from pySim.sms import SMS_SUBMIT, AddressField
|
||||
from pySim.cat import (ProactiveCommand, CommandDetails, DeviceIdentities,
|
||||
BearerDescription, BufferSize, UiccTransportLevel,
|
||||
OtherAddress, ChannelData, ChannelDataLength, ChannelStatus,
|
||||
Result, LocationInformation)
|
||||
|
||||
from pySim.bip import Proact, ProactChannels, terminal_profile
|
||||
|
||||
|
||||
class _EchoServer:
|
||||
"""behold, my tiny threaded TCP echo server listening on 127.0.0.1:<port>"""
|
||||
def __init__(self):
|
||||
self._srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._srv.bind(('127.0.0.1', 0))
|
||||
self._srv.listen(1)
|
||||
self.port = self._srv.getsockname()[1]
|
||||
self.accepted = threading.Event()
|
||||
self._conns = []
|
||||
self._stop = False
|
||||
threading.Thread(target=self._run, daemon=True).start()
|
||||
|
||||
def _run(self):
|
||||
try:
|
||||
conn, _ = self._srv.accept()
|
||||
except OSError:
|
||||
return
|
||||
self._conns.append(conn)
|
||||
self.accepted.set()
|
||||
while not self._stop:
|
||||
try:
|
||||
data = conn.recv(4096)
|
||||
except OSError:
|
||||
break
|
||||
if not data:
|
||||
break
|
||||
conn.sendall(data)
|
||||
|
||||
def close(self):
|
||||
self._stop = True
|
||||
for s in [self._srv] + self._conns:
|
||||
try:
|
||||
s.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _pcmd(children_tlvs):
|
||||
"""Assemble D0 proactive-command TLV from child IE bytes,
|
||||
decode it like transport does after a FETCH"""
|
||||
body = b''.join(children_tlvs)
|
||||
pdu = h2b('D0') + bytes([len(body)]) + body
|
||||
return ProactiveCommand().from_tlv(pdu)
|
||||
|
||||
|
||||
def _open_channel(port, ip='127.0.0.1', cmd_nr=1):
|
||||
a, b, c, d = (int(x) for x in ip.split('.'))
|
||||
return _pcmd([
|
||||
CommandDetails(decoded={'command_number': cmd_nr, 'type_of_command': 'open_channel',
|
||||
'command_qualifier': 3}).to_tlv(),
|
||||
DeviceIdentities(decoded={'source_dev_id': 'uicc', 'dest_dev_id': 'terminal'}).to_tlv(),
|
||||
BearerDescription(decoded={'bearer_type': 'default', 'bearer_parameters': b''}).to_tlv(),
|
||||
BufferSize(decoded=1024).to_tlv(),
|
||||
UiccTransportLevel(decoded={'protocol_type': 'tcp_uicc_client_remote',
|
||||
'port_number': port}).to_tlv(),
|
||||
OtherAddress(decoded={'type_of_address': 'ipv4',
|
||||
'address': bytes([a, b, c, d])}).to_tlv(),
|
||||
])
|
||||
|
||||
|
||||
def _open_channel_raw(extra_ies, cmd_nr=1):
|
||||
"""OPEN CHANNEL with only the head data"""
|
||||
return _pcmd([
|
||||
CommandDetails(decoded={'command_number': cmd_nr, 'type_of_command': 'open_channel',
|
||||
'command_qualifier': 3}).to_tlv(),
|
||||
DeviceIdentities(decoded={'source_dev_id': 'uicc', 'dest_dev_id': 'terminal'}).to_tlv(),
|
||||
BearerDescription(decoded={'bearer_type': 'default', 'bearer_parameters': b''}).to_tlv(),
|
||||
BufferSize(decoded=1024).to_tlv(),
|
||||
] + extra_ies)
|
||||
|
||||
|
||||
def _send_data(payload, chan='channel_1', cmd_nr=1):
|
||||
return _pcmd([
|
||||
CommandDetails(decoded={'command_number': cmd_nr, 'type_of_command': 'send_data',
|
||||
'command_qualifier': 1}).to_tlv(),
|
||||
DeviceIdentities(decoded={'source_dev_id': 'uicc', 'dest_dev_id': chan}).to_tlv(),
|
||||
ChannelData(decoded=b2h(payload)).to_tlv(),
|
||||
])
|
||||
|
||||
|
||||
def _receive_data(length, chan='channel_1', cmd_nr=1):
|
||||
return _pcmd([
|
||||
CommandDetails(decoded={'command_number': cmd_nr, 'type_of_command': 'receive_data',
|
||||
'command_qualifier': 0}).to_tlv(),
|
||||
DeviceIdentities(decoded={'source_dev_id': 'uicc', 'dest_dev_id': chan}).to_tlv(),
|
||||
ChannelDataLength(decoded=length).to_tlv(),
|
||||
])
|
||||
|
||||
|
||||
def _close_channel(chan='channel_1', cmd_nr=1):
|
||||
return _pcmd([
|
||||
CommandDetails(decoded={'command_number': cmd_nr, 'type_of_command': 'close_channel',
|
||||
'command_qualifier': 0}).to_tlv(),
|
||||
DeviceIdentities(decoded={'source_dev_id': 'uicc', 'dest_dev_id': chan}).to_tlv(),
|
||||
])
|
||||
|
||||
|
||||
def _first(til, cls):
|
||||
return next((x for x in til if isinstance(x, cls)), None)
|
||||
|
||||
|
||||
class BipRelayRoundTripTest(unittest.TestCase):
|
||||
"""Drive the fixed Proact handlers (blocking sockets) with synthetic
|
||||
proactive commands against a local echo server and assert a byte round-trip
|
||||
plus the channel bookkeeping / error handling."""
|
||||
|
||||
def setUp(self):
|
||||
self.echo = _EchoServer()
|
||||
self.addCleanup(self.echo.close)
|
||||
self.events = []
|
||||
self.proact = Proact(data_available_sink=self.events.append)
|
||||
self.addCleanup(self._close_all_channels)
|
||||
|
||||
def _close_all_channels(self):
|
||||
for chan in list(self.proact.channels.channels.values()):
|
||||
try:
|
||||
chan.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _open(self, cmd_nr=1):
|
||||
til = self.proact.handle_OpenChannel(_open_channel(self.echo.port, cmd_nr=cmd_nr))
|
||||
# every TLV in the response must serialise (the transport does exactly
|
||||
# this to post the TERMINAL RESPONSE)
|
||||
b''.join(x.to_tlv() for x in til)
|
||||
return til
|
||||
|
||||
def test_open_send_receive_roundtrip(self):
|
||||
# OPEN CHANNEL -> socket connected, channel 1 opened, link established
|
||||
til = self._open()
|
||||
self.assertTrue(self.echo.accepted.wait(timeout=2.0))
|
||||
self.assertIn(1, self.proact.channels.channels)
|
||||
cd = _first(til, CommandDetails)
|
||||
self.assertEqual(cd.decoded['type_of_command'], 'open_channel')
|
||||
# TS 102 223 6.8.2 TERMINAL RESPONSE device id: terminal -> UICC
|
||||
self.assertEqual(b2h(_first(til, DeviceIdentities).to_tlv()), '82028281')
|
||||
# channel status: channel 1, link established
|
||||
self.assertEqual(_first(til, ChannelStatus).decoded, '8100')
|
||||
self.assertEqual(_first(til, Result).decoded['general_result'], 'performed_successfully')
|
||||
|
||||
# SEND DATA -> bytes written to the socket, echo server sends them back
|
||||
payload = b'Hello SCP81 relay - opaque TLS record bytes'
|
||||
til = self.proact.handle_SendData(_send_data(payload))
|
||||
b''.join(x.to_tlv() for x in til)
|
||||
# channel data length in the response = free Tx space, FF = ">255"
|
||||
self.assertEqual(_first(til, ChannelDataLength).decoded, 255)
|
||||
self.assertEqual(_first(til, Result).decoded['general_result'], 'performed_successfully')
|
||||
|
||||
# RECEIVE DATA -> drain the bytes back to the "card". real card
|
||||
# uses data-available event, we poll the buffer
|
||||
# and may need several RECEIVE DATA commands, as the spec allows.
|
||||
got = bytearray()
|
||||
deadline = time.monotonic() + 3.0
|
||||
while len(got) < len(payload) and time.monotonic() < deadline:
|
||||
chan = self.proact.channels.channels[1]
|
||||
chan.wait_rx(1.0)
|
||||
til = self.proact.handle_ReceiveData(_receive_data(len(payload) - len(got)))
|
||||
b''.join(x.to_tlv() for x in til)
|
||||
self.assertEqual(b2h(_first(til, DeviceIdentities).to_tlv()), '82028281')
|
||||
got += h2b(_first(til, ChannelData).decoded)
|
||||
self.assertEqual(bytes(got), payload, "byte round-trip through the BIP relay")
|
||||
|
||||
# CLOSE CHANNEL -> socket closed, bookkeeping cleared
|
||||
til = self.proact.handle_CloseChannel(_close_channel())
|
||||
b''.join(x.to_tlv() for x in til)
|
||||
self.assertEqual(_first(til, Result).decoded['general_result'], 'performed_successfully')
|
||||
self.assertNotIn(1, self.proact.channels.channels)
|
||||
|
||||
def test_data_available_event_envelope(self):
|
||||
# The empty->non-empty Rx transition raises ENVELOPE EVENT DOWNLOAD
|
||||
self._open()
|
||||
self.assertTrue(self.echo.accepted.wait(timeout=2.0))
|
||||
payload = b'PONG'
|
||||
self.proact.handle_SendData(_send_data(payload))
|
||||
chan = self.proact.channels.channels[1]
|
||||
self.assertGreater(chan.wait_rx(2.0), 0)
|
||||
# give the reader thread a beat to invoke the sink
|
||||
deadline = time.monotonic() + 2.0
|
||||
while not self.events and time.monotonic() < deadline:
|
||||
time.sleep(0.01)
|
||||
self.assertEqual(len(self.events), 1, "one data-available event on the empty->non-empty edge")
|
||||
env = h2b(self.events[0])
|
||||
# d6 0e | 99 01 09 (event: data available) | 82 02 82 81 terminal->UICC
|
||||
# | b8 02 81 00 (channel 1 established) | b7 01 XX bytes available
|
||||
self.assertEqual(b2h(env[:15]), 'd60e99010982028281b8028100b701')
|
||||
self.assertGreaterEqual(env[15], 1)
|
||||
self.assertLessEqual(env[15], len(payload))
|
||||
|
||||
def test_channel_number_from_device_identities(self):
|
||||
# Two channels, not the old hardcoded 1
|
||||
e2 = _EchoServer()
|
||||
self.addCleanup(e2.close)
|
||||
self.proact.handle_OpenChannel(_open_channel(self.echo.port))
|
||||
# open a second channel with a second echo server
|
||||
til2 = self.proact.handle_OpenChannel(_open_channel(e2.port))
|
||||
self.assertEqual(sorted(self.proact.channels.channels), [1, 2])
|
||||
self.assertEqual(_first(til2, ChannelStatus).decoded, '8200') # channel 2, established
|
||||
|
||||
# SEND DATA addressed to channel_2 must reach the second socket
|
||||
self.assertTrue(e2.accepted.wait(timeout=2.0))
|
||||
self.proact.handle_SendData(_send_data(b'two', chan='channel_2'))
|
||||
chan2 = self.proact.channels.channels[2]
|
||||
self.assertGreater(chan2.wait_rx(2.0), 0)
|
||||
til = self.proact.handle_ReceiveData(_receive_data(3, chan='channel_2'))
|
||||
self.assertEqual(h2b(_first(til, ChannelData).decoded), b'two')
|
||||
# ..and nothing on chan 1
|
||||
self.assertEqual(self.proact.channels.channels[1].available_rx(), 0)
|
||||
|
||||
def test_commands_on_closed_channel_report_bip_error(self):
|
||||
# SEND/RECEIVE/CLOSE on a channel that was never opened must be rejected
|
||||
# with a BIP error
|
||||
for til in (self.proact.handle_SendData(_send_data(b'x', chan='channel_4')),
|
||||
self.proact.handle_ReceiveData(_receive_data(1, chan='channel_4')),
|
||||
self.proact.handle_CloseChannel(_close_channel(chan='channel_4'))):
|
||||
b''.join(x.to_tlv() for x in til)
|
||||
res = _first(til, Result).decoded
|
||||
self.assertEqual(res['general_result'], 'bearer_independent_protocol_error')
|
||||
self.assertEqual(res['additional_information'], 'channel_id_not_valid')
|
||||
|
||||
def test_receive_more_than_available_is_missing_info(self):
|
||||
# terminal must NOT wait if fewer than the requested bytes are buffered,
|
||||
# eturns what it has with "performed with missing information".
|
||||
self._open()
|
||||
self.assertTrue(self.echo.accepted.wait(timeout=2.0))
|
||||
til = self.proact.handle_ReceiveData(_receive_data(10))
|
||||
b''.join(x.to_tlv() for x in til)
|
||||
self.assertEqual(_first(til, Result).decoded['general_result'],
|
||||
'performed_with_missing_information')
|
||||
self.assertEqual(h2b(_first(til, ChannelData).decoded), b'')
|
||||
self.assertEqual(_first(til, ChannelDataLength).decoded, 0)
|
||||
|
||||
|
||||
class OpenChannelRefusalTest(unittest.TestCase):
|
||||
"""Refusal is a TERMINAL RESPONSE, not an exception, raising takes the whole
|
||||
proactive session down and leaves the card wondering why"""
|
||||
|
||||
ADDR = OtherAddress(decoded={'type_of_address': 'ipv4', 'address': bytes([127, 0, 0, 1])})
|
||||
TCP = UiccTransportLevel(decoded={'protocol_type': 'tcp_uicc_client_remote', 'port_number': 1234})
|
||||
|
||||
def setUp(self):
|
||||
self.proact = Proact()
|
||||
self.addCleanup(self._close_all_channels)
|
||||
|
||||
def _close_all_channels(self):
|
||||
for chan in list(self.proact.channels.channels.values()):
|
||||
try:
|
||||
chan.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _assert_refused(self, til, additional_information, chan_nr=0):
|
||||
b''.join(x.to_tlv() for x in til) # must serialise, the transport posts it
|
||||
res = _first(til, Result).decoded
|
||||
self.assertEqual(res['general_result'], 'bearer_independent_protocol_error')
|
||||
self.assertEqual(res['additional_information'], additional_information)
|
||||
self.assertEqual(_first(til, ChannelStatus).decoded, '%02x00' % chan_nr) # 8.56
|
||||
self.assertIsNotNone(_first(til, BearerDescription)) # 6.8.20
|
||||
self.assertIsNotNone(_first(til, BufferSize)) # 6.8.21
|
||||
self.assertEqual(b2h(_first(til, DeviceIdentities).to_tlv()), '82028281') # 6.8.2
|
||||
|
||||
def test_transport_level(self):
|
||||
cases = [[self.ADDR.to_tlv()]] # absent, 6.6.27.x Optional
|
||||
for proto in ('udp_uicc_client_remote', 'tcp_uicc_server', 'udp_uicc_client_local',
|
||||
'tcp_uicc_client_local', 'direct_channel'): # not TCP client remote
|
||||
tl = UiccTransportLevel(decoded={'protocol_type': proto, 'port_number': 1234})
|
||||
cases.append([tl.to_tlv(), self.ADDR.to_tlv()])
|
||||
for extra in cases:
|
||||
with self.subTest(extra=b2h(extra[0])):
|
||||
self._assert_refused(self.proact.handle_OpenChannel(_open_channel_raw(extra)),
|
||||
'requested_uicc_if_transp_level_not_available')
|
||||
|
||||
def test_destination_address(self):
|
||||
v6 = OtherAddress(decoded={'type_of_address': 'ipv6', 'address': bytes(16)})
|
||||
for extra in ([self.TCP.to_tlv()], # absent
|
||||
[self.TCP.to_tlv(), v6.to_tlv()]): # not IPv4
|
||||
with self.subTest(extra=len(extra)):
|
||||
self._assert_refused(self.proact.handle_OpenChannel(_open_channel_raw(extra)),
|
||||
'no_specific_cause')
|
||||
|
||||
def test_no_channel_left(self):
|
||||
for _ in range(7): # 6.4.27.2, 6.4.27.3
|
||||
self.proact.channels.channel_create()
|
||||
cmd = _open_channel_raw([self.TCP.to_tlv(), self.ADDR.to_tlv()])
|
||||
self._assert_refused(self.proact.handle_OpenChannel(cmd), 'no_channel_availabile')
|
||||
|
||||
def test_connect_failure(self):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # port nothing listens on
|
||||
s.bind(('127.0.0.1', 0))
|
||||
dead_port = s.getsockname()[1]
|
||||
s.close()
|
||||
til = self.proact.handle_OpenChannel(_open_channel(dead_port))
|
||||
self._assert_refused(til, 'channel_closed', chan_nr=1) # 6.4.30
|
||||
self.assertEqual(self.proact.channels.channels, {}) # channel given back
|
||||
|
||||
|
||||
class ProvideLocalInformationTest(unittest.TestCase):
|
||||
"""TS 102 223 6.8.7: only 00 gets a data object; the rest keeps the empty result."""
|
||||
|
||||
def _cmd(self, qualifier):
|
||||
return _pcmd([
|
||||
CommandDetails(decoded={'command_number': 1, 'type_of_command': 'provide_local_info',
|
||||
'command_qualifier': qualifier}).to_tlv(),
|
||||
DeviceIdentities(decoded={'source_dev_id': 'uicc', 'dest_dev_id': 'terminal'}).to_tlv()])
|
||||
|
||||
def test_location(self):
|
||||
til = Proact().handle_ProvideLocalInformation(self._cmd(0x00))
|
||||
b''.join(x.to_tlv() for x in til)
|
||||
self.assertEqual(_first(til, Result).decoded['general_result'], 'performed_successfully')
|
||||
self.assertEqual(b2h(_first(til, LocationInformation).to_tlv()), '930762f21000010001')
|
||||
self.assertEqual(b2h(_first(til, DeviceIdentities).to_tlv()), '82028281') # 6.8.2
|
||||
|
||||
def test_other_qualifiers_get_no_data_object(self):
|
||||
for qualifier in (0x01, 0x03, 0x04, 0x1a):
|
||||
with self.subTest(command_qualifier=qualifier):
|
||||
til = Proact().handle_ProvideLocalInformation(self._cmd(qualifier))
|
||||
b''.join(x.to_tlv() for x in til)
|
||||
self.assertIsNone(_first(til, LocationInformation))
|
||||
|
||||
def test_location_is_configurable(self):
|
||||
til = Proact(location=h2b('26f8100539')).handle_ProvideLocalInformation(self._cmd(0x00))
|
||||
self.assertEqual(b2h(_first(til, LocationInformation).to_tlv()), '930526f8100539')
|
||||
|
||||
|
||||
class TerminalProfileTest(unittest.TestCase):
|
||||
"""TS 102 223 5.2, one bit per CAT facility"""
|
||||
|
||||
def setUp(self):
|
||||
self.profile = terminal_profile()
|
||||
|
||||
def byte(self, n):
|
||||
return self.profile[n - 1] # 1-based, as 5.2 numbers them
|
||||
|
||||
def test_announced(self):
|
||||
self.assertEqual(len(self.profile), 32)
|
||||
self.assertEqual(self.byte(1), 0x13) # profile download, SMS-PP download b2+b5
|
||||
self.assertEqual(self.byte(4), 0x02) # SEND SHORT MESSAGE
|
||||
self.assertEqual(self.byte(5) & 0x01, 0x01) # SET UP EVENT LIST
|
||||
self.assertEqual(self.byte(6), 0x0c) # events: data available, channel status
|
||||
self.assertEqual(self.byte(12), 0x1f) # OPEN/CLOSE CHANNEL, RECEIVE/SEND DATA, STATUS
|
||||
self.assertEqual(self.byte(13) >> 5, ProactChannels.MAX_CHANNELS)
|
||||
self.assertEqual(self.byte(14), 0x60) # class ND, class NK
|
||||
self.assertEqual(self.byte(17), 0x01) # TCP, UICC client mode, remote
|
||||
|
||||
def test_not_announced(self):
|
||||
self.assertEqual(self.byte(3) & 0x60, 0) # POLL INTERVAL, POLLING OFF
|
||||
self.assertEqual(self.byte(4) & 0xc0, 0) # PROVIDE LOCAL INFORMATION, NMR
|
||||
self.assertEqual(self.byte(12) & 0xe0, 0) # SERVICE SEARCH/INFORMATION, DECLARE SERVICE
|
||||
self.assertEqual(self.byte(14) & 0x1f, 0) # no characters down the display
|
||||
for n in (7, 9, 10, 11, 15, 16, 18): # class "a", class "d", display, ESN/IMEISV
|
||||
self.assertEqual(self.byte(n), 0)
|
||||
|
||||
def test_channel_count(self):
|
||||
self.assertEqual(terminal_profile(3)[12] >> 5, 3)
|
||||
with self.assertRaises(ValueError): # 8.56: 1 to 7
|
||||
terminal_profile(8)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class BipSinkTest(unittest.TestCase):
|
||||
"""Both sinks are optional, a driver with no SMS path at all must not crash and burn
|
||||
with a card that sends one, and one that has one must get the PDU."""
|
||||
|
||||
def _submit(self):
|
||||
return SMS_SUBMIT(tp_da=AddressField('12345', 'unknown', 'unknown'),
|
||||
tp_ud=b'\x01\x02', tp_udl=2, tp_dcs=0xf6)
|
||||
|
||||
def test_sinks_default_to_none(self):
|
||||
p = Proact()
|
||||
self.assertIsNone(p.sms_sink)
|
||||
|
||||
def test_mo_sms_goes_to_the_sink(self):
|
||||
seen = []
|
||||
Proact(sms_sink=seen.append).send_sms_via_smpp(self._submit())
|
||||
self.assertEqual(len(seen), 1)
|
||||
|
||||
def test_no_sms_sink_drops_instead_of_raising(self):
|
||||
with self.assertLogs('pySim.bip', level='INFO'):
|
||||
Proact().send_sms_via_smpp(self._submit())
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the CAT (Card Application Toolkit) COMPREHENSION-TLV data objects"""
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Eric Wild <ewild@sysmocom.de>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# IEs not properly coverd by test_tlvs.py
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
from osmocom.utils import b2h, h2b
|
||||
|
||||
from pySim.cat import IMEI, IMEISV, AccessTechnology, SupportedRadioAccessTechnologies
|
||||
|
||||
|
||||
class IMEI_Test(unittest.TestCase):
|
||||
"""TS 102 223 8.20: the IMEI IE is 8 bytes, coded as valie part of Mobile Identity IE from 124 008"""
|
||||
|
||||
IMEI_15 = '123456789012345'
|
||||
ENCODED = '94081a32547698103254'
|
||||
|
||||
def test_encode_is_eight_bytes(self):
|
||||
"""15 digits in 8 byte: 16 nibbles, one is type/parity framing."""
|
||||
tlv = IMEI(decoded=self.IMEI_15).to_tlv()
|
||||
self.assertEqual(b2h(tlv), self.ENCODED)
|
||||
self.assertEqual(tlv[1], 0x08) # spec len 8
|
||||
self.assertEqual(len(tlv) - 2, 8)
|
||||
|
||||
def test_first_octet_framing(self):
|
||||
"""TS 24.008 table 10.5.4"""
|
||||
octet1 = IMEI(decoded=self.IMEI_15).to_tlv()[2]
|
||||
self.assertEqual(octet1 & 0x07, 2) # IMEI
|
||||
self.assertEqual((octet1 >> 3) & 0x01, 1) # odd
|
||||
self.assertEqual(octet1 >> 4, 1) # digit 1
|
||||
|
||||
def test_decodes_to_the_raw_imei(self):
|
||||
"""strip framing nibble"""
|
||||
ie = IMEI()
|
||||
ie.from_tlv(h2b(self.ENCODED))
|
||||
self.assertEqual(ie.decoded, self.IMEI_15)
|
||||
|
||||
def test_even_digit_count_uses_the_end_mark(self):
|
||||
""""end marker, IMEISV case"""
|
||||
ie = IMEI(decoded='1234567890123456')
|
||||
tlv = ie.to_tlv()
|
||||
self.assertEqual(tlv[2] >> 3 & 0x01, 0) # even
|
||||
self.assertEqual(tlv[-1] >> 4, 0x0f) # end mark
|
||||
back = IMEI()
|
||||
back.from_tlv(tlv)
|
||||
self.assertEqual(back.decoded, '1234567890123456')
|
||||
|
||||
|
||||
class IMEISV_Test(unittest.TestCase):
|
||||
"""TS 102 223 8.74, no fixed len, end marker"""
|
||||
|
||||
IMEISV_16 = '1234567890123456'
|
||||
ENCODED = 'e2091332547698103254f6'
|
||||
|
||||
def test_encode(self):
|
||||
self.assertEqual(b2h(IMEISV(decoded=self.IMEISV_16).to_tlv()), self.ENCODED)
|
||||
|
||||
def test_type_of_identity_and_end_mark(self):
|
||||
value = IMEISV(decoded=self.IMEISV_16).to_tlv()[2:]
|
||||
self.assertEqual(value[0] & 0x07, 3) # IMEISV
|
||||
self.assertEqual((value[0] >> 3) & 0x01, 0) # even
|
||||
self.assertEqual(value[-1] >> 4, 0x0f) # end mark
|
||||
self.assertEqual(len(value), 9)
|
||||
|
||||
def test_decode(self):
|
||||
ie = IMEISV()
|
||||
ie.from_tlv(h2b(self.ENCODED))
|
||||
self.assertEqual(ie.decoded, self.IMEISV_16)
|
||||
|
||||
|
||||
class SupportedRadioAccessTechnologies_Test(unittest.TestCase):
|
||||
"""TS 102 223 8.105"""
|
||||
|
||||
def test_encode_technology_enabled(self):
|
||||
"""The flag used to have a bitmask of 0 so enabled -> 00 (that is disabled..)"""
|
||||
ie = SupportedRadioAccessTechnologies(
|
||||
decoded=[{'technology': 'eutran', 'state': {'enabled': True}}])
|
||||
self.assertEqual(b2h(ie.to_tlv()), 'b4020801')
|
||||
|
||||
def test_encode_technology_disabled(self):
|
||||
ie = SupportedRadioAccessTechnologies(
|
||||
decoded=[{'technology': 'eutran', 'state': {'enabled': False}}])
|
||||
self.assertEqual(b2h(ie.to_tlv()), 'b4020800')
|
||||
|
||||
def test_decode_technology(self):
|
||||
"""old 0 bitmask = all enabled, no way to disable"""
|
||||
for encoded, enabled in [('b4020800', False), ('b4020801', True)]:
|
||||
with self.subTest(encoded=encoded):
|
||||
ie = SupportedRadioAccessTechnologies()
|
||||
ie.from_tlv(h2b(encoded))
|
||||
self.assertEqual(ie.decoded[0]['technology'], 'eutran')
|
||||
self.assertEqual(ie.decoded[0]['state']['enabled'], enabled)
|
||||
|
||||
def test_decode_technology_multiple(self):
|
||||
ie = SupportedRadioAccessTechnologies()
|
||||
ie.from_tlv(h2b('b40408010000'))
|
||||
self.assertEqual([(e['technology'], e['state']['enabled']) for e in ie.decoded],
|
||||
[('eutran', True), ('gsm', False)])
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -27,7 +27,6 @@ import pySim.ts_31_102
|
||||
import pySim.ts_31_103
|
||||
import pySim.ts_51_011
|
||||
import pySim.sysmocom_sja2
|
||||
import pySim.sysmocom_sjs1
|
||||
import pySim.gsm_r
|
||||
import pySim.cdma_ruim
|
||||
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
|
||||
import unittest
|
||||
import logging
|
||||
import hashlib
|
||||
from types import SimpleNamespace
|
||||
from osmocom.utils import b2h, h2b
|
||||
from osmocom.tlv import bertlv_encode_len
|
||||
|
||||
from pySim.global_platform import *
|
||||
from pySim.global_platform.scp import *
|
||||
@@ -286,41 +283,6 @@ class SCP03_Test_AES256_33(SCP03_Test, unittest.TestCase):
|
||||
# FIXME: test auth with random (0x60) vs pseudo-random (0x70) challenge
|
||||
|
||||
|
||||
class KeyComponentBlock_Test(unittest.TestCase):
|
||||
"""Tests for the kcb of GP CardSpec v2.3
|
||||
- Table 11-70 kcv that required padding, preceded by its clear-text length
|
||||
- Table 11-71 no padding required"""
|
||||
|
||||
def setUp(self):
|
||||
# SCP02 (3DES DEK, 8 byte blocks), same vectors as SCP02_Test
|
||||
self.scp02 = SCP02(card_keys=ck_3des_70)
|
||||
self.scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
|
||||
self.scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
|
||||
self.scp02.gen_ext_auth_apdu()
|
||||
# SCP03 (AES DEK, 16 byte blocks), same vectors as SCP03_Test_AES128_11
|
||||
self.scp03 = SCP03(card_keys=KEYSET_AES128)
|
||||
self.scp03.gen_init_update_apdu(h2b('b13e5f938fc108c4'))
|
||||
self.scp03.parse_init_update_resp(h2b('000000000000000000003003703eb51047495b249f66c484c1d2ef1948000002'))
|
||||
self.scp03.gen_ext_auth_apdu(0x11)
|
||||
|
||||
def test_encrypt_decrypt_key(self):
|
||||
for scp in (self.scp02, self.scp03):
|
||||
bs = scp.sk.blocksize
|
||||
for keylen in range(1, 3 * bs + 1):
|
||||
with self.subTest(scp=type(scp).__name__, keylen=keylen):
|
||||
key = bytes(range(keylen))
|
||||
kcb = scp.encrypt_key(key)
|
||||
if keylen % bs:
|
||||
# Table 11-70: <length of clear key component> || <encrypted padded value>
|
||||
self.assertEqual(kcb[0], keylen)
|
||||
self.assertEqual((len(kcb) - 1) % bs, 0)
|
||||
self.assertEqual(len(kcb) - 1, keylen + (bs - keylen % bs))
|
||||
else:
|
||||
# Table 11-71: only the encrypted key component value
|
||||
self.assertEqual(len(kcb), keylen)
|
||||
self.assertEqual(scp.decrypt_key(kcb), key)
|
||||
|
||||
|
||||
class SCP03_KCV_Test(unittest.TestCase):
|
||||
def test_kcv(self):
|
||||
self.assertEqual(compute_kcv('aes', KEYSET_AES128.enc), h2b('C35280'))
|
||||
@@ -328,204 +290,6 @@ class SCP03_KCV_Test(unittest.TestCase):
|
||||
self.assertEqual(compute_kcv('aes', KEYSET_AES128.dek), h2b('840DE5'))
|
||||
|
||||
|
||||
class PutKey_PSK_Test(unittest.TestCase):
|
||||
"""Tests for the PUT KEY command data field encoding, in particular the PSK TLS ('85') key data
|
||||
field defined by GlobalPlatform Amendment B (Remote Application Management over HTTP) Table 3-13."""
|
||||
|
||||
# SCP80 TLS-PSK example key from the do_put_key docstring (16 bytes)
|
||||
PSK_CLEAR = h2b('303132333435363738393a3b3c3d3e3f')
|
||||
# its DEK ciphertext + Table 3-13 KCV with SCP02 session set up below
|
||||
PSK_CIPHERED = h2b('15abf1fe16ccc5aa13743394442942cd')
|
||||
PSK_KCV = h2b('06125d') # = SHA-1(PSK_CLEAR)[:3]
|
||||
|
||||
def setUp(self):
|
||||
# SCP02 with the same vectors as SCP02_Test, so that the whole PUT KEY data field is reproducible.
|
||||
self.scp02 = SCP02(card_keys=ck_3des_70)
|
||||
self.scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
|
||||
self.scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
|
||||
self.scp02.gen_ext_auth_apdu()
|
||||
|
||||
def test_psk_kcv_is_sha1(self):
|
||||
# GP Amendment B Table 3-13: KCV = 3 most significant bytes of SHA-1(clear key)
|
||||
self.assertEqual(compute_kcv('tls_psk', self.PSK_CLEAR), hashlib.sha1(self.PSK_CLEAR).digest()[:3])
|
||||
self.assertEqual(compute_kcv('tls_psk', self.PSK_CLEAR), self.PSK_KCV)
|
||||
|
||||
def test_encode_psk_framing_golden(self):
|
||||
# assert the exact Table 3-13 layout
|
||||
# 85 | L1 | L2 | <ciphered> | 03 | <SHA-1(clear)[:3]>
|
||||
clear = self.PSK_CLEAR
|
||||
ciphered = h2b('aabbccddeeff00112233445566778899') # arbitrary 16-byte ciphertext
|
||||
kcv = hashlib.sha1(clear).digest()[:3]
|
||||
field = ADF_SD.encode_key_data_psk(clear, ciphered, kcv)
|
||||
# 85 L1 L2 <---------- ciphered -----------> 03 <-kcv->
|
||||
self.assertEqual(b2h(field),'85' '11' '10' 'aabbccddeeff00112233445566778899' '03' + b2h(kcv))
|
||||
self.assertEqual(b2h(field),'851110aabbccddeeff0011223344556677889903' + '06125d')
|
||||
|
||||
def test_psk_golden_over_scp02(self):
|
||||
# Full PUT KEY data field (KVN 0x40 + single PSK key) enciphered with the SCP02 DEK.
|
||||
keys = [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
|
||||
'kcv': compute_kcv('tls_psk', self.PSK_CLEAR)}]
|
||||
data = ADF_SD.build_put_key_data(0x40, keys, self.scp02)
|
||||
self.assertEqual(b2h(data),
|
||||
'40' '85' '11' '10' + b2h(self.PSK_CIPHERED) + '03' + b2h(self.PSK_KCV))
|
||||
|
||||
def test_wrong_basic_format_differs(self):
|
||||
# regression test, the generic "Basic format" does NOT match Table 3-13 for a PSK key
|
||||
# rejected by card with with 6a88
|
||||
wrong_basic = ADF_SD.encode_key_data_basic('tls_psk', self.PSK_CIPHERED, b'')
|
||||
right_psk = ADF_SD.encode_key_data_psk(self.PSK_CLEAR, self.PSK_CIPHERED, self.PSK_KCV)
|
||||
self.assertEqual(b2h(wrong_basic), '8510' + b2h(self.PSK_CIPHERED) + '00')
|
||||
self.assertEqual(b2h(right_psk), '8511' '10' + b2h(self.PSK_CIPHERED) + '03' + b2h(self.PSK_KCV))
|
||||
self.assertNotEqual(wrong_basic, right_psk)
|
||||
|
||||
def test_key_component_block_length_is_bertlv(self):
|
||||
# GP CardSpec v2.3.1 Section 11.8.2.3.1: all lengths ofPUT KEY are always BER TLV coded
|
||||
for kcb_len, exp_len_field in [(127, '7f'), (128, '8180'), (129, '8181'), (256, '820100')]:
|
||||
with self.subTest(kcb_len=kcb_len):
|
||||
kcb = bytes(kcb_len)
|
||||
field = ADF_SD.encode_key_data_basic('rsa_modulus_n', kcb, b'')
|
||||
self.assertEqual(b2h(field), 'a2' + exp_len_field + b2h(kcb) + '00')
|
||||
# 85 field of Amendment B Table 3-13 uses the same coding
|
||||
# single byte inner length (clear key < 128) == block kcb_len bytes long
|
||||
psk = ADF_SD.encode_key_data_psk(bytes(120), bytes(kcb_len - 1), b'')
|
||||
self.assertEqual(b2h(psk)[:2 + len(exp_len_field)], '85' + exp_len_field)
|
||||
|
||||
def test_basic_format_unchanged(self):
|
||||
# as before
|
||||
for kt, clear in [('des', h2b('404142434445464748494a4b4c4d4e4f')),
|
||||
('aes', h2b('000102030405060708090a0b0c0d0e0f'))]:
|
||||
ciph = self.scp02.encrypt_key(clear)
|
||||
kcv = compute_kcv(kt, clear)
|
||||
via_construct = build_construct(ADF_SD.KeyDataBasic, {'key_type': kt, 'kcb': b2h(ciph), 'kcv': b2h(kcv)})
|
||||
via_helper = ADF_SD.encode_key_data_basic(kt, ciph, kcv)
|
||||
self.assertEqual(via_helper, via_construct)
|
||||
|
||||
def test_psk_padding_no_double_length(self):
|
||||
# A PSK key whose length is not a multiple of the DEK block size (DES: 8) is right-padded before
|
||||
# ciphering. Table 3-13 states the clear key length (L2) in the '85' DO itself, so the ciphered
|
||||
# key field is the bare cryptogram:
|
||||
# - ciphered field == padded ciphertext (no duplicated length prefix),
|
||||
# - clear key == first L2 bytes.
|
||||
for keylen in (18, 20):
|
||||
with self.subTest(keylen=keylen):
|
||||
clear = bytes(range(keylen))
|
||||
padded_len = keylen + (-keylen % 8)
|
||||
field = ADF_SD.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear,
|
||||
'kcv': compute_kcv('tls_psk', clear)}], self.scp02)[1:]
|
||||
self.assertEqual(field[0], 0x85)
|
||||
l1 = field[1]
|
||||
l2 = field[2]
|
||||
self.assertEqual(l2, keylen) # single-byte BER length of clear key
|
||||
ciphered = field[3:3 + (l1 - 1)] # value = L2 (1 byte) || ciphered key
|
||||
self.assertEqual(len(ciphered), padded_len) # padded to the 8-byte DES block size
|
||||
self.assertEqual(l1, 1 + padded_len) # no duplicated length prefix
|
||||
self.assertEqual(self.scp02.dek_decrypt(ciphered)[:keylen], clear)
|
||||
|
||||
def test_psk_clear_key_is_not_padded_in_place(self):
|
||||
# padding the bytearray in place would make L2 the padded length,
|
||||
# then stored as key material and rejected thanks to the KCV
|
||||
clear = h2b('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d') # 30, not %8
|
||||
kcv = compute_kcv('tls_psk', clear)
|
||||
field = ADF_SD.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': clear,
|
||||
'kcv': kcv}], self.scp02)[1:]
|
||||
self.assertEqual(len(clear), 30)
|
||||
self.assertEqual(field[2], 30) # L2 == clear key length, not 32
|
||||
self.assertEqual(self.scp02.dek_decrypt(field[3:3 + field[1] - 1])[:30], clear)
|
||||
|
||||
def test_kcv_suppressed(self):
|
||||
# --suppress-key-check -> KCV length 00 and no KCV bytes
|
||||
field = ADF_SD.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
|
||||
'kcv': b''}], self.scp02)[1:]
|
||||
self.assertEqual(b2h(field), '8511' '10' + b2h(self.PSK_CIPHERED) + '00')
|
||||
|
||||
def test_multikey_psk_plus_des_dek(self):
|
||||
# load a PSK TLS key (KID 1, Amendment B format) together with its DES DEK
|
||||
# (KID 2, Basic format) in one PUT KEY.
|
||||
# Verify the concatenated data field parses back into the two components with proper type formats.
|
||||
dek = h2b('404142434445464748494a4b4c4d4e4f')
|
||||
keys = [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR, 'kcv': compute_kcv('tls_psk', self.PSK_CLEAR)},
|
||||
{'key_type': 'des', 'clear_key': dek, 'kcv': compute_kcv('des', dek)}]
|
||||
data = ADF_SD.build_put_key_data(0x40, keys, self.scp02)
|
||||
|
||||
b = data
|
||||
self.assertEqual(b[0], 0x40) # KVN
|
||||
b = b[1:]
|
||||
# component 1: PSK TLS (Table 3-13)
|
||||
self.assertEqual(b[0], 0x85)
|
||||
self.assertEqual(b[1], 0x11) # L1 = 17
|
||||
self.assertEqual(b[2], 0x10) # L2 = 16 (clear key length)
|
||||
self.assertEqual(b[3:3 + 16], self.PSK_CIPHERED)
|
||||
self.assertEqual(b[3 + 16], 0x03) # KCV length
|
||||
self.assertEqual(b[3 + 16 + 1:3 + 16 + 1 + 3], self.PSK_KCV)
|
||||
b = b[3 + 16 + 1 + 3:]
|
||||
# component 2: DES DEK (Basic format)
|
||||
self.assertEqual(b[0], 0x80) # key type des
|
||||
kcb_len = b[1]
|
||||
self.assertEqual(kcb_len, 16)
|
||||
self.assertEqual(b[2:2 + kcb_len], self.scp02.encrypt_key(dek))
|
||||
b = b[2 + kcb_len:]
|
||||
self.assertEqual(b[0], 0x03) # KCV length
|
||||
self.assertEqual(b[1:1 + 3], compute_kcv('des', dek))
|
||||
self.assertEqual(b[1 + 3:], b'') # no trailing bytes
|
||||
|
||||
def test_no_scp_leaves_key_clear(self):
|
||||
# During personalization (no SCP) the key is not enciphered, framing still follows Table 3-13.
|
||||
field = ADF_SD.build_put_key_data(0x40, [{'key_type': 'tls_psk', 'clear_key': self.PSK_CLEAR,
|
||||
'kcv': self.PSK_KCV}], None)[1:]
|
||||
self.assertEqual(b2h(field), '8511' '10' + b2h(self.PSK_CLEAR) + '03' + b2h(self.PSK_KCV))
|
||||
|
||||
|
||||
class PutKey_Length_Test(unittest.TestCase):
|
||||
"""Tests for the length of the PUT KEY command APDU. Lc of GP CardSpec v2.3 Table 11-64 is a
|
||||
single byte, so an oversized key data field cannot be sent."""
|
||||
|
||||
class _FakeSccForPutKey():
|
||||
"""mock scc: replays hardcoded status word + records the APDUs sent."""
|
||||
def __init__(self, scp=None, max_cmd_len=255):
|
||||
self.sent = []
|
||||
self.scp = scp
|
||||
self.max_cmd_len = max_cmd_len
|
||||
|
||||
def send_apdu_checksw(self, apdu, sw='9000'):
|
||||
self.sent.append(apdu)
|
||||
return ('', '9000')
|
||||
|
||||
# KVN, key type, two byte BER length of the key component block, KCV length; KCV suppressed
|
||||
FRAMING = 1 + 1 + 2 + 1
|
||||
|
||||
@staticmethod
|
||||
def key(nbytes: int):
|
||||
return [{'key_type': 'rsa_modulus_n', 'clear_key': bytes(nbytes), 'kcv': b''}]
|
||||
|
||||
def test_lc_matches_data_field(self):
|
||||
# largest key component block that still fits without a secure channel
|
||||
scc = self._FakeSccForPutKey()
|
||||
ADF_SD.put_key(scc, 0, 0x40, 1, self.key(255 - self.FRAMING))
|
||||
apdu = scc.sent[0]
|
||||
self.assertEqual(apdu[:8], '80D80001')
|
||||
lc = int(apdu[8:10], 16)
|
||||
self.assertEqual(lc, 255) # Lc ...
|
||||
self.assertEqual(len(apdu[10:-2]) // 2, lc) # ... and it matches the actual data field
|
||||
|
||||
def test_oversized_key_data_raises(self):
|
||||
# real world fat example: RSA-2048 modulus does not fit, led to 3 nibble Lc 106,
|
||||
# which silently shifted and broke the whole APDU by half a byte.
|
||||
scc = self._FakeSccForPutKey()
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
ADF_SD.put_key(scc, 0, 0x40, 1, self.key(256))
|
||||
self.assertIn('262', str(ctx.exception))
|
||||
self.assertIn('255', str(ctx.exception))
|
||||
self.assertEqual(scc.sent, []) # nothing was sent to the card
|
||||
|
||||
def test_secure_channel_overhead_lowers_the_limit(self):
|
||||
# scc.max_cmd_len shrinks by the C-MAC + encryption padding of active SCP
|
||||
scc = self._FakeSccForPutKey(max_cmd_len=239)
|
||||
ADF_SD.put_key(scc, 0, 0x40, 1, self.key(239 - self.FRAMING))
|
||||
self.assertEqual(int(scc.sent[0][8:10], 16), 239)
|
||||
with self.assertRaises(ValueError):
|
||||
ADF_SD.put_key(scc, 0, 0x40, 1, self.key(239 - self.FRAMING + 1))
|
||||
|
||||
|
||||
class Install_param_Test(unittest.TestCase):
|
||||
def test_gen_install_parameters(self):
|
||||
load_parameters = gen_install_parameters(256, 256, '010001001505000000000000000000000000')
|
||||
@@ -534,326 +298,5 @@ class Install_param_Test(unittest.TestCase):
|
||||
load_parameters = gen_install_parameters()
|
||||
self.assertEqual(load_parameters, 'c900')
|
||||
|
||||
class SCP_Overhead_Test(unittest.TestCase):
|
||||
"""SCP.overhead varies according to the current security level:
|
||||
C-MAC + at level >= 3 the worst-case padding!
|
||||
"""
|
||||
|
||||
def _scp02(self, security_level):
|
||||
scp = SCP02(card_keys=ck_3des_70)
|
||||
scp.sk = Scp02SessionKeys(0x0001, ck_3des_70)
|
||||
scp.security_level = security_level
|
||||
return scp
|
||||
|
||||
def _scp03(self, security_level, s_mode=8):
|
||||
scp = SCP03(card_keys=KEYSET_AES128, s_mode=s_mode)
|
||||
scp.sk = Scp03SessionKeys(KEYSET_AES128, b'\x00' * s_mode, b'\x11' * s_mode)
|
||||
scp.security_level = security_level
|
||||
return scp
|
||||
|
||||
def test_scp02(self):
|
||||
self.assertEqual(self._scp02(0x00).overhead, 0) # no wrapping at all
|
||||
self.assertEqual(self._scp02(0x01).overhead, 8) # C-MAC
|
||||
self.assertEqual(self._scp02(0x03).overhead, 16) # C-MAC + C-DEC: pad80 to 8, largest fit 239
|
||||
|
||||
def test_scp03_s8(self):
|
||||
self.assertEqual(self._scp03(0x00).overhead, 0)
|
||||
self.assertEqual(self._scp03(0x01).overhead, 8)
|
||||
self.assertEqual(self._scp03(0x03).overhead, 16) # pad80 to 16 within 247 -> 240, minus pad byte
|
||||
self.assertEqual(self._scp03(0x33).overhead, 16) # R-MAC/R-ENC add no *command* overhead
|
||||
|
||||
def test_scp03_s16(self):
|
||||
self.assertEqual(self._scp03(0x01, s_mode=16).overhead, 16)
|
||||
self.assertEqual(self._scp03(0x03, s_mode=16).overhead, 32) # pad80 to 16 within 239 -> 224, minus pad byte
|
||||
|
||||
|
||||
class SCP_Lc_Limit_Test_Base(unittest.TestCase):
|
||||
"""Test wrap_cmd_apdu() boundary handling: data of (255 - overhead) must produce Lc <= 255 else ValueError"""
|
||||
|
||||
def _load_apdu(self, data_len):
|
||||
return h2b('80E80000') + bytes([data_len]) + b'\xa5' * data_len
|
||||
|
||||
def _check_boundary(self, scp):
|
||||
fits = 255 - scp.overhead
|
||||
wrapped = scp.wrap_cmd_apdu(self._load_apdu(fits))
|
||||
self.assertLessEqual(wrapped[4], 255)
|
||||
self.assertEqual(len(wrapped), 5 + wrapped[4]) # case #3: header + Lc bytes, no Le
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
scp.wrap_cmd_apdu(self._load_apdu(fits + 1))
|
||||
self.assertIn('Lc', str(ctx.exception))
|
||||
|
||||
|
||||
class SCP02_Lc_Limit_Test(SCP_Lc_Limit_Test_Base):
|
||||
"""Same session vectors as SCP02_Auth_Test"""
|
||||
|
||||
def setUp(self):
|
||||
self.scp02 = SCP02(card_keys=ck_3des_70)
|
||||
self.scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
|
||||
self.scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
|
||||
self.scp02.gen_ext_auth_apdu()
|
||||
|
||||
def test_cmac_only(self):
|
||||
self.scp02.security_level = 0x01
|
||||
self._check_boundary(self.scp02) # 247 fits, 248 raises
|
||||
|
||||
def test_cmac_cdec(self):
|
||||
self.scp02.security_level = 0x03
|
||||
self._check_boundary(self.scp02) # 239 fits (-> Lc 248), 240 raises (would be 256)
|
||||
|
||||
def test_cmac_cdec_wrapped_lc(self):
|
||||
# my actual failing case: 240 bytes at level 3
|
||||
self.scp02.security_level = 0x03
|
||||
wrapped = self.scp02.wrap_cmd_apdu(self._load_apdu(239))
|
||||
self.assertEqual(wrapped[4], 248) # 239 -> pad80 -> 240 ciphertext + 8 mac
|
||||
|
||||
|
||||
class SCP03_Lc_Limit_Test(SCP_Lc_Limit_Test_Base):
|
||||
"""Session keys derived directly"""
|
||||
|
||||
def _scp03(self, security_level, s_mode):
|
||||
scp = SCP03(card_keys=KEYSET_AES128, s_mode=s_mode)
|
||||
scp.sk = Scp03SessionKeys(KEYSET_AES128, b'\x00' * s_mode, b'\x11' * s_mode)
|
||||
scp.security_level = security_level
|
||||
return scp
|
||||
|
||||
def test_s8_cmac_only(self):
|
||||
self._check_boundary(self._scp03(0x01, 8)) # 247 fits, 248 raises
|
||||
|
||||
def test_s8_cmac_cdec(self):
|
||||
self._check_boundary(self._scp03(0x03, 8)) # 239 fits, 240 raises
|
||||
|
||||
def test_s16_cmac_only(self):
|
||||
self._check_boundary(self._scp03(0x01, 16)) # 239 fits, 240 raises
|
||||
|
||||
def test_s16_cmac_cdec(self):
|
||||
self._check_boundary(self._scp03(0x03, 16)) # 223 fits, 224 raises
|
||||
|
||||
|
||||
class _FakeSccForLoad:
|
||||
"""mock lchan.scc: records LOAD APDUs, optionally wrapping them through a real SCP
|
||||
instance first where the Lc overflow used to blow up"""
|
||||
|
||||
def __init__(self, max_cmd_len=255, scp=None):
|
||||
self.max_cmd_len = max_cmd_len
|
||||
self.scp = scp
|
||||
self.sent = []
|
||||
self.wrapped = []
|
||||
|
||||
def send_apdu_checksw(self, apdu, sw='9000'):
|
||||
self.sent.append(apdu.lower())
|
||||
if self.scp:
|
||||
self.wrapped.append(self.scp.wrap_cmd_apdu(h2b(apdu)))
|
||||
return ('', '9000')
|
||||
|
||||
|
||||
class Load_ChunkLen_Test(unittest.TestCase):
|
||||
"""ADF_SD.load() chunking: block size must use scc.max_cmd_len"""
|
||||
|
||||
payload = b'\xaa' * 500 # actual real world case LOAD TLV: C4 + 8201f4 + 500 = 504 total
|
||||
|
||||
def _blocks(self, scc):
|
||||
"""Get (p1, p2, lc) from LOAD APDU"""
|
||||
for apdu in scc.sent:
|
||||
self.assertEqual(apdu[0:4], '80e8')
|
||||
yield int(apdu[4:6], 16), int(apdu[6:8], 16), int(apdu[8:10], 16)
|
||||
|
||||
def test_default_no_scp(self):
|
||||
"""Without SCP the old 240 byte block size is kept, no idea what else might rely on this number"""
|
||||
scc = _FakeSccForLoad(max_cmd_len=255)
|
||||
ADF_SD.load(scc, self.payload)
|
||||
blocks = list(self._blocks(scc))
|
||||
self.assertEqual([b[2] for b in blocks], [240, 240, 24])
|
||||
self.assertEqual([b[0] for b in blocks], [0x00, 0x00, 0x80]) # P1: last block flagged
|
||||
self.assertEqual([b[1] for b in blocks], [0, 1, 2]) # P2: block num
|
||||
|
||||
def test_default_scp02_level3(self):
|
||||
"""max_cmd_len 239 (SCP02 lvl 3) squeezes the blocks"""
|
||||
scc = _FakeSccForLoad(max_cmd_len=239)
|
||||
ADF_SD.load(scc, self.payload)
|
||||
self.assertEqual([b[2] for b in list(self._blocks(scc))], [239, 239, 26])
|
||||
|
||||
def test_explicit_chunk_len(self):
|
||||
scc = _FakeSccForLoad(max_cmd_len=255)
|
||||
ADF_SD.load(scc, self.payload, chunk_len=100)
|
||||
self.assertEqual([b[2] for b in list(self._blocks(scc))], [100] * 5 + [4])
|
||||
|
||||
def test_explicit_chunk_len_too_large(self):
|
||||
scc = _FakeSccForLoad(max_cmd_len=239)
|
||||
with self.assertRaises(ValueError):
|
||||
ADF_SD.load(scc, self.payload, chunk_len=240)
|
||||
self.assertEqual(scc.sent, []) # nothing sent!
|
||||
|
||||
def test_explicit_chunk_len_zero(self):
|
||||
scc = _FakeSccForLoad(max_cmd_len=255)
|
||||
with self.assertRaises(ValueError):
|
||||
ADF_SD.load(scc, self.payload, chunk_len=0)
|
||||
|
||||
def test_end_to_end_scp02_level3(self):
|
||||
"""original failure: 286 byte CAP + SCP02 lvl 3"""
|
||||
scp02 = SCP02(card_keys=ck_3des_70)
|
||||
scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8'))
|
||||
scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3'))
|
||||
scp02.gen_ext_auth_apdu()
|
||||
scp02.security_level = 0x03
|
||||
scc = _FakeSccForLoad(max_cmd_len=255 - scp02.overhead, scp=scp02)
|
||||
ADF_SD.load(scc, b'\x5a' * 286)
|
||||
self.assertEqual(len(scc.sent), 2) # 289 byte TLV in blocks of 239
|
||||
for wrapped in scc.wrapped:
|
||||
self.assertLessEqual(wrapped[4], 255)
|
||||
|
||||
# Real Card Data (GET DATA '66'), as returned by sja5 + euicc
|
||||
CARD_DATA_V211 = ('6631732f06072a864886fc6b01600c060a2a864886fc6b0202010163090607'
|
||||
'2a864886fc6b03640b06092a864886fc6b040215')
|
||||
CARD_DATA_V22 = ('663b733906072a864886fc6b01600b06092a864886fc6b020202630906072a86'
|
||||
'4886fc6b03640b06092a864886fc6b040370640b06092a864886fc6b04810400')
|
||||
|
||||
|
||||
class _FakeScc:
|
||||
"""mock lchan.scc: replays scripted (data, sw) pairs + records the APDUs sent."""
|
||||
|
||||
def __init__(self, responses, card_data=CARD_DATA_V211):
|
||||
self._responses = list(responses)
|
||||
self._card_data = card_data
|
||||
self.sent = []
|
||||
|
||||
def get_data(self, cla, tag):
|
||||
if self._card_data is None:
|
||||
raise SwMatchError('6a88', '9000')
|
||||
return self._card_data, '9000'
|
||||
|
||||
def send_apdu(self, apdu):
|
||||
self.sent.append(apdu.lower())
|
||||
if not self._responses:
|
||||
raise AssertionError('get_status sent unexpected APDU: %s' % apdu)
|
||||
return self._responses.pop(0)
|
||||
|
||||
|
||||
class GpVersion_Test(unittest.TestCase):
|
||||
"""GP version from Card Recognition Data, which v2.1.1/v2.3.1 section 7.4.1.3
|
||||
require to be present. The OID under tag 60 is {globalPlatform 2 v...}."""
|
||||
|
||||
def test_decode_real_cards(self):
|
||||
self.assertEqual(decode_gp_version(h2b(CARD_DATA_V211)), (2, 1, 1))
|
||||
self.assertEqual(decode_gp_version(h2b(CARD_DATA_V22)), (2, 2))
|
||||
|
||||
def test_unknown_oid_is_none(self):
|
||||
self.assertIsNone(decode_gp_version(h2b('66097307060512345678')))
|
||||
|
||||
def test_tag_lists_follow_the_spec_tables(self):
|
||||
"""table 11-36 applications, table 11-37 for load files"""
|
||||
self.assertEqual(b2h(get_status_tag_list('isd')), '5c074f9f70c5cfc4cc')
|
||||
self.assertEqual(b2h(get_status_tag_list('applications')), '5c074f9f70c5cfc4cc')
|
||||
self.assertEqual(b2h(get_status_tag_list('files')), '5c054f9f70cecc')
|
||||
self.assertEqual(b2h(get_status_tag_list('files_and_modules')), '5c064f9f70ce84cc')
|
||||
# C5 never load files, 84 never applications
|
||||
self.assertNotIn('c5', b2h(get_status_tag_list('files')))
|
||||
self.assertNotIn('84', b2h(get_status_tag_list('applications'))[4:])
|
||||
|
||||
|
||||
class GetStatus_Pagination_Test(unittest.TestCase):
|
||||
"""GPC v2.3.1 section 11.4.3.2 table 11-38 GET STATUS pagination test
|
||||
|
||||
Card answers 6310 when further matches are pending; command reissued with
|
||||
P2 bit 1 "next occurrence" set. Tied to T=0 handling pySim/transport, which
|
||||
used to swallow that 6310 and replied with GET RESPONSE, so page 2 was never fetched."""
|
||||
|
||||
ENTRY_1 = 'e3074f05a000000151'
|
||||
ENTRY_2 = 'e3074f05a000000152'
|
||||
|
||||
def _aids(self, grd_list):
|
||||
return [b2h(grd.to_dict()['gp_registry_related_data'][0]['application_aid']) for grd in grd_list]
|
||||
|
||||
def test_single_page(self):
|
||||
scc = _FakeScc([(self.ENTRY_1, '9000')])
|
||||
grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(scc.sent, ['80f24002024f0000'])
|
||||
self.assertEqual(self._aids(grd_list), ['a000000151'])
|
||||
|
||||
def test_two_pages(self):
|
||||
"""6310 -> reissue with P2 bit 1 set -> 9000, both pages in result"""
|
||||
scc = _FakeScc([(self.ENTRY_1, '6310'), (self.ENTRY_2, '9000')])
|
||||
grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(scc.sent, ['80f24002024f0000',
|
||||
'80f24003024f0000'])
|
||||
self.assertEqual(self._aids(grd_list), ['a000000151', 'a000000152'])
|
||||
|
||||
def test_three_pages_keep_p2_next_occurrence(self):
|
||||
scc = _FakeScc([(self.ENTRY_1, '6310'), (self.ENTRY_2, '6310'), (self.ENTRY_1, '9000')])
|
||||
grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual([a[6:8] for a in scc.sent], ['02', '03', '03'])
|
||||
self.assertEqual(len(grd_list), 3)
|
||||
|
||||
def test_no_match_returns_empty(self):
|
||||
"""6A88 "referenced data not found" is empty result not failure."""
|
||||
scc = _FakeScc([('', '6a88')])
|
||||
self.assertEqual(ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)), [])
|
||||
|
||||
def test_v211_card_gets_no_tag_list(self):
|
||||
"""v2.1.1 section 9.4.2.3 has no tag list,not send a tag list"""
|
||||
scc = _FakeScc([(self.ENTRY_1, '9000')], card_data=CARD_DATA_V211)
|
||||
ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(scc.sent, ['80f24002024f0000'])
|
||||
self.assertNotIn('5c', scc.sent[0][8:])
|
||||
|
||||
def test_v22_card_gets_a_tag_list(self):
|
||||
scc = _FakeScc([(self.ENTRY_1, '9000')], card_data=CARD_DATA_V22)
|
||||
ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00'])
|
||||
|
||||
def test_unknown_version_gets_no_tag_list(self):
|
||||
"""If the card will not say, assume the conservative form that works everywhere."""
|
||||
scc = _FakeScc([(self.ENTRY_1, '9000')], card_data=None)
|
||||
ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(scc.sent, ['80f24002024f0000'])
|
||||
|
||||
def test_v22_card_rejecting_tag_list_falls_back(self):
|
||||
"""card announcing v2.2+ that still answers 6A80 to the tag list."""
|
||||
scc = _FakeScc([('', '6a80'), (self.ENTRY_1, '9000')], card_data=CARD_DATA_V22)
|
||||
grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00',
|
||||
'80f24002024f0000'])
|
||||
self.assertEqual(self._aids(grd_list), ['a000000151'])
|
||||
|
||||
def test_aid_search_qualifier(self):
|
||||
scc = _FakeScc([(self.ENTRY_1, '9000')])
|
||||
ADF_SD.get_status(scc, 'applications', 'a000000087', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(scc.sent, ['80f24002074f05a00000008700'])
|
||||
|
||||
def test_6a80_is_reported_on_a_v211_card(self):
|
||||
"""no tag list -> 6A80 is error"""
|
||||
scc = _FakeScc([('', '6a80')], card_data=CARD_DATA_V211)
|
||||
with self.assertRaises(SwMatchError) as ctx:
|
||||
ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(ctx.exception.sw_actual, '6a80')
|
||||
|
||||
def test_unexpected_sw_is_not_silently_truncated(self):
|
||||
"""partial is not complete result"""
|
||||
scc = _FakeScc([(self.ENTRY_1, '6310'), ('', '6982')])
|
||||
with self.assertRaises(SwMatchError) as ctx:
|
||||
ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(ctx.exception.sw_actual, '6982')
|
||||
|
||||
def test_v22_card_answering_6a88_to_the_tag_list_falls_back(self):
|
||||
"""6A88 is the other GET STATUS error condition of table 11-39, section 11.4.2.3
|
||||
says we may get get an error status. 6A88 to the tag-list attempt should be retried
|
||||
without it or we get nothing"""
|
||||
scc = _FakeScc([('', '6a88'), (self.ENTRY_1, '9000')], card_data=CARD_DATA_V22)
|
||||
grd_list = ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))
|
||||
self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00',
|
||||
'80f24002024f0000'])
|
||||
self.assertEqual(self._aids(grd_list), ['a000000151'])
|
||||
|
||||
def test_v22_card_with_a_genuinely_empty_subset(self):
|
||||
"""...and when the retry answers 6A88, the list really is empty."""
|
||||
scc = _FakeScc([('', '6a88'), ('', '6a88')], card_data=CARD_DATA_V22)
|
||||
self.assertEqual(ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc)), [])
|
||||
self.assertEqual(len(scc.sent), 2)
|
||||
|
||||
def test_6a88_after_a_page_keeps_that_page(self):
|
||||
"""6A88 is "no more matches" after we have data, we're done"""
|
||||
scc = _FakeScc([(self.ENTRY_1, '6310'), ('', '6a88')], card_data=CARD_DATA_V22)
|
||||
self.assertEqual(self._aids(ADF_SD.get_status(scc, 'applications', version=ADF_SD.gp_version(scc))),
|
||||
['a000000151'])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -20,20 +20,11 @@
|
||||
|
||||
import unittest
|
||||
import logging
|
||||
import cmd2
|
||||
from packaging import version
|
||||
from pySim.log import PySimLogger
|
||||
import io
|
||||
import sys
|
||||
from inspect import currentframe, getframeinfo
|
||||
|
||||
if version.parse(cmd2.__version__) >= version.parse("3.0.0"):
|
||||
from cmd2 import Color # pylint: disable=no-name-in-module
|
||||
YELLOW = Color.YELLOW
|
||||
else: # cmd2>=2.6.2
|
||||
from cmd2 import Fg # pylint: disable=no-name-in-module
|
||||
YELLOW = Fg.YELLOW
|
||||
|
||||
log = PySimLogger.get(__name__)
|
||||
|
||||
TEST_MSG_DEBUG = "this is a debug message"
|
||||
@@ -46,17 +37,6 @@ expected_message = None
|
||||
|
||||
class PySimLogger_Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# PySimLogger.setup() is global, so a print callback left installed here fires for
|
||||
# every PySimLogger message emitted by any test module that runs later in the same process
|
||||
# ... where it asserts against a stale 'expected_message' and fails a test that has nothing
|
||||
# to do with logging. Great fun!
|
||||
# Restore before each test.
|
||||
saved = (PySimLogger.print_callback, PySimLogger.verbose)
|
||||
def _restore():
|
||||
PySimLogger.print_callback, PySimLogger.verbose = saved
|
||||
self.addCleanup(_restore)
|
||||
|
||||
def __test_01_safe_defaults_one(self, callback, message:str):
|
||||
# When log messages are sent to an unconfigured PySimLogger class, we expect the unmodified message being
|
||||
# logged to stdout, just as if it were printed via a normal print() statement.
|
||||
@@ -137,18 +117,5 @@ class PySimLogger_Test(unittest.TestCase):
|
||||
expected_message = "CRITICAL: " + TEST_MSG_CRITICAL
|
||||
log.critical(TEST_MSG_CRITICAL)
|
||||
|
||||
def test_05_color(self):
|
||||
# A color is either
|
||||
# - raw escape sequence
|
||||
# - cmd2 color object
|
||||
global expected_message
|
||||
expected_message = "\033[33mWARNING: " + TEST_MSG_WARNING + "\033[0m"
|
||||
|
||||
PySimLogger.setup(self._test_print_callback, {logging.WARN: "\033[33m"})
|
||||
log.warning(TEST_MSG_WARNING)
|
||||
|
||||
PySimLogger.setup(self._test_print_callback, {logging.WARN: YELLOW})
|
||||
log.warning(TEST_MSG_WARNING) # don't leak cmd2 Color StrEnum
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -300,292 +300,5 @@ class SmsOtaTestCase(OtaTestCase):
|
||||
self.assertEqual(d.last_status_word, t['response']['last_status_word'])
|
||||
self.assertEqual(d.last_response_data, t['response']['last_response_data'])
|
||||
|
||||
|
||||
######################################################################
|
||||
# Expanded Remote Application data format (ETSI TS 102 226 Section 5.2)
|
||||
######################################################################
|
||||
|
||||
class BerTlvLengthTestCase(unittest.TestCase):
|
||||
"""The definite-length BER-TLV length field (ISO/IEC 8825-1) used by the
|
||||
expanded format, incl. the multi-byte (>127) forms (0x81xx / 0x82xxxx)."""
|
||||
def test_roundtrip(self):
|
||||
# (length value, expected encoded bytes)
|
||||
vectors = [
|
||||
(0, '00'),
|
||||
(1, '01'),
|
||||
(127, '7f'),
|
||||
(128, '8180'),
|
||||
(198, '81c6'), # big ~198 byte GET STATUS registry from a sja5
|
||||
(255, '81ff'),
|
||||
(256, '820100'),
|
||||
(65535, '82ffff'),
|
||||
]
|
||||
for length, encoded in vectors:
|
||||
with self.subTest(length=length):
|
||||
built = BerTlvLen.build(length)
|
||||
self.assertEqual(b2h(built), encoded)
|
||||
self.assertEqual(BerTlvLen.parse(built), length)
|
||||
|
||||
|
||||
class ExpandedCmdTestCase(unittest.TestCase):
|
||||
"""Command Scripting template TS 102 226 5.2.1"""
|
||||
|
||||
def test_single_capdu_golden(self):
|
||||
# GP GET STATUS, Le=00, TS 102 226 5.2.1.1 R-APDU
|
||||
out = encode_expanded_cmd(h2b('80f24002024f0000'))
|
||||
# aa = TS 101 220 table 7.18 Command Scripting template tag
|
||||
# 0a = length 10
|
||||
# 22 = TS 101 220 table 7.19 C-APDU tag
|
||||
# 08 = length
|
||||
# + C-APDU
|
||||
self.assertEqual(b2h(out), 'aa0a220880f24002024f0000')
|
||||
|
||||
def test_multi_capdu_golden(self):
|
||||
out = encode_expanded_cmd([h2b('80f24002024f0000'), h2b('00a40004023f0000')])
|
||||
self.assertEqual(b2h(out), 'aa14220880f24002024f0000220800a40004023f0000')
|
||||
|
||||
def test_multibyte_length_golden(self):
|
||||
# C-APDU: 4 header + 1 Lc + 195 data = 200 bytes.
|
||||
# 200 byte C-APDU forces long form BER lengths:
|
||||
# C-APDU TLV, 200 -> 81c8 + template 203 -> 81cb
|
||||
capdu = h2b('80f24000') + bytes([195]) + bytes(range(195))
|
||||
self.assertEqual(len(capdu), 200)
|
||||
out = encode_expanded_cmd(capdu)
|
||||
# aa 81 cb | 22 81 c8 | <200 byte capdu>
|
||||
self.assertEqual(b2h(out[:6]), 'aa81cb2281c8')
|
||||
self.assertEqual(out[6:], capdu)
|
||||
|
||||
def test_roundtrip(self):
|
||||
for apdus in [[h2b('80f24002024f0000')],
|
||||
[h2b('00a40004023f00'), h2b('80f24002024f0000')],
|
||||
[h2b('00'*250)]]:
|
||||
with self.subTest(n=len(apdus)):
|
||||
out = encode_expanded_cmd(apdus)
|
||||
parsed = ExpandedCmd.parse(out)
|
||||
self.assertEqual([h2b(c.c_apdu) for c in parsed.commands], apdus)
|
||||
|
||||
|
||||
class ExpandedRespTestCase(unittest.TestCase):
|
||||
"""Decoding of the Response Scripting template (TS 102 226 5.2.2)."""
|
||||
|
||||
def test_registry_golden(self):
|
||||
# real card case: GET STATUS returns a ~198 byte registry TLV + SW 9000
|
||||
# R-APDU = 198 data + 2 SW = 200/81c8
|
||||
# 'number of executed' TLV 80 01 01.
|
||||
registry = bytes(range(198))
|
||||
data = ExpandedRemoteResp.build(dict(body=dict(
|
||||
num_executed=dict(number_of_commands=1),
|
||||
responses=[dict(r_apdu=dict(response_data=b2h(registry), status_word='9000'))])))
|
||||
# ab | 81 ce | 80 01 01 | 23 81 c8 | <198 data> 90 00
|
||||
self.assertEqual(b2h(data[:9]), 'ab81ce8001012381c8')
|
||||
dec = decode_expanded_resp(data)
|
||||
self.assertEqual(dec.number_of_commands, 1)
|
||||
self.assertEqual(len(dec.commands), 1)
|
||||
self.assertEqual(dec.last_status_word, '9000')
|
||||
self.assertEqual(dec.last_response_data, b2h(registry))
|
||||
|
||||
def test_status_only_golden(self):
|
||||
# last command, no response data, SW 6132
|
||||
data = ExpandedRemoteResp.build(dict(body=dict(
|
||||
num_executed=dict(number_of_commands=1),
|
||||
responses=[dict(r_apdu=dict(response_data='', status_word='6132'))])))
|
||||
self.assertEqual(b2h(data), 'ab0780010123026132')
|
||||
dec = decode_expanded_resp(data)
|
||||
self.assertEqual(dec.last_status_word, '6132')
|
||||
self.assertEqual(dec.last_response_data, '')
|
||||
|
||||
def test_multi_command(self):
|
||||
data = ExpandedRemoteResp.build(dict(body=dict(
|
||||
num_executed=dict(number_of_commands=2),
|
||||
responses=[dict(r_apdu=dict(response_data='6f21', status_word='9000')),
|
||||
dict(r_apdu=dict(response_data='', status_word='6a82'))])))
|
||||
dec = decode_expanded_resp(data)
|
||||
self.assertEqual(dec.number_of_commands, 2)
|
||||
self.assertEqual([(c.status_word, c.response_data) for c in dec.commands],
|
||||
[('9000', '6f21'), ('6a82', '')])
|
||||
# last == final R-APDU, error status included
|
||||
self.assertEqual(dec.last_status_word, '6a82')
|
||||
self.assertEqual(dec.last_response_data, '')
|
||||
|
||||
def test_bad_format(self):
|
||||
# ab | 06 | 80 01 01 | 90 01 01
|
||||
data = h2b('ab06800101900101')
|
||||
dec = decode_expanded_resp(data)
|
||||
self.assertEqual(str(dec.bad_format), 'unknown_tag')
|
||||
self.assertIsNone(dec.last_status_word)
|
||||
|
||||
def test_immediate_action_error(self):
|
||||
# ab | 06 | 80 01 01 | 81 01 01
|
||||
data = h2b('ab06800101810101')
|
||||
dec = decode_expanded_resp(data)
|
||||
self.assertEqual(str(dec.immediate_action_response), 'suspension_error')
|
||||
|
||||
def test_script_chaining_error(self):
|
||||
# ab | 06 | 80 01 01 | 83 01 02
|
||||
data = h2b('ab06800101830102')
|
||||
dec = decode_expanded_resp(data)
|
||||
self.assertEqual(str(dec.script_chaining_response), 'not_supported')
|
||||
|
||||
def test_truncation_is_flagged(self):
|
||||
"""TS 102 226 5.2.1.1: SW 62F1 means the C-APDU response data was truncated, and
|
||||
"this shall terminate the processing of the command list"
|
||||
halves are invisible in the R-APDU list, truncated + aborted script must not pass as complete"""
|
||||
# second command truncated -> processing stopped at that point
|
||||
data = ExpandedRemoteResp.build(dict(body=dict(
|
||||
num_executed=dict(number_of_commands=2),
|
||||
responses=[dict(r_apdu=dict(response_data='6f21', status_word='9000')),
|
||||
dict(r_apdu=dict(response_data='aabb', status_word='62f1'))])))
|
||||
dec = decode_expanded_resp(data)
|
||||
self.assertTrue(dec.truncated)
|
||||
self.assertEqual(dec.last_status_word, '62f1')
|
||||
|
||||
def test_untruncated_response_is_not_flagged(self):
|
||||
data = ExpandedRemoteResp.build(dict(body=dict(
|
||||
num_executed=dict(number_of_commands=1),
|
||||
responses=[dict(r_apdu=dict(response_data='6f21', status_word='9000'))])))
|
||||
self.assertFalse(decode_expanded_resp(data).truncated)
|
||||
# 62xx that is not 62F1 is warning, not truncation
|
||||
data = ExpandedRemoteResp.build(dict(body=dict(
|
||||
num_executed=dict(number_of_commands=1),
|
||||
responses=[dict(r_apdu=dict(response_data='', status_word='6282'))])))
|
||||
self.assertFalse(decode_expanded_resp(data).truncated)
|
||||
|
||||
|
||||
class ExpandedIndefiniteTestCase(unittest.TestCase):
|
||||
"""Indef len coding of expanded format TS 102 226 tables
|
||||
5.2a/5.10a; cmd tag AE, resp tag AF.
|
||||
Golden vectors captured from live eUICC over SCP81/HTTPS."""
|
||||
|
||||
def test_cmd_single_golden(self):
|
||||
# RAM GET DATA 80CA00E000 -> AE 80 | 22 05 80ca00e000 | 00 00
|
||||
out = encode_expanded_cmd(h2b('80ca00e000'), length_coding='indefinite')
|
||||
self.assertEqual(b2h(out), 'ae80220580ca00e0000000')
|
||||
|
||||
def test_cmd_multi_golden(self):
|
||||
# RFM: SELECT MF / SELECT EF.ICCID / READ BINARY, each in one C-APDU
|
||||
# TLV, wrapped in indef Command Scripting template
|
||||
out = encode_expanded_cmd([h2b('00a4000c023f00'), h2b('00a4000c022fe2'),
|
||||
h2b('00b000000a')], length_coding='indefinite')
|
||||
self.assertEqual(b2h(out),
|
||||
'ae80220700a4000c023f00220700a4000c022fe2220500b000000a0000')
|
||||
|
||||
def test_cmd_definite_is_default(self):
|
||||
# The default/explicit definite keeps the tag AA
|
||||
self.assertEqual(encode_expanded_cmd(h2b('80ca00e000')),
|
||||
encode_expanded_cmd(h2b('80ca00e000'), length_coding='definite'))
|
||||
self.assertEqual(b2h(encode_expanded_cmd(h2b('80ca00e000'))), 'aa07220580ca00e000')
|
||||
|
||||
def test_cmd_invalid_length_coding(self):
|
||||
with self.assertRaises(ValueError):
|
||||
encode_expanded_cmd(h2b('80ca00e000'), length_coding='bogus')
|
||||
|
||||
def test_resp_rfm_golden(self):
|
||||
# AF 80 | 23 02 9000 | 23 02 9000 | 23 0c <ICCID> 9000 | 00 00
|
||||
# indef res has no "number of executed" TLV.
|
||||
dec = decode_expanded_resp(h2b(
|
||||
'af80' '23029000' '23029000' '230c988812010000408608149000' '0000'))
|
||||
self.assertEqual(len(dec.commands), 3)
|
||||
self.assertEqual([(c.status_word, c.response_data) for c in dec.commands],
|
||||
[('9000', ''), ('9000', ''), ('9000', '98881201000040860814')])
|
||||
self.assertEqual(dec.last_status_word, '9000')
|
||||
self.assertEqual(dec.last_response_data, '98881201000040860814')
|
||||
# report the R-APDU count instead
|
||||
self.assertEqual(dec.number_of_commands, 3)
|
||||
|
||||
def test_resp_ram_golden(self):
|
||||
# RAM GET DATA: R-APDU carrying the SD key info TLV + SW.
|
||||
resp = ('af80' '2334e030c00403308810c00402308810c00401308810c00402408810'
|
||||
'c00401408510c00403018810c00402018810c004010188109000' '0000')
|
||||
dec = decode_expanded_resp(h2b(resp))
|
||||
self.assertEqual(len(dec.commands), 1)
|
||||
self.assertEqual(dec.last_status_word, '9000')
|
||||
self.assertEqual(dec.last_response_data,
|
||||
'e030c00403308810c00402308810c00401308810c00402408810'
|
||||
'c00401408510c00403018810c00402018810c00401018810')
|
||||
|
||||
def test_resp_truncated_is_rejected(self):
|
||||
# last byte chopped off: the end-of-contents marker is incomplete
|
||||
good = h2b('af80' '23029000' '230c988812010000408608149000' '0000')
|
||||
for cut in (1, 2, 3):
|
||||
with self.subTest(cut=cut):
|
||||
with self.assertRaises(ValueError):
|
||||
decode_expanded_resp(good[:-cut])
|
||||
|
||||
def test_resp_definite_still_parses(self):
|
||||
# same decoder still handles the definite AB template.
|
||||
dec = decode_expanded_resp(h2b('ab0780010123029000'))
|
||||
self.assertEqual(dec.number_of_commands, 1)
|
||||
self.assertEqual(dec.last_status_word, '9000')
|
||||
|
||||
def test_resp_indefinite_bad_format(self):
|
||||
# AF 80 | 90 01 01 | 00 00 unknown_tag no R-APDU
|
||||
dec = decode_expanded_resp(h2b('af8090010100 00'.replace(' ', '')))
|
||||
self.assertEqual(str(dec.bad_format), 'unknown_tag')
|
||||
self.assertIsNone(dec.last_status_word)
|
||||
|
||||
def test_resp_missing_eoc_raises(self):
|
||||
# AF 80 | 23 02 9000 without end-of-contents.
|
||||
with self.assertRaises(ValueError):
|
||||
decode_expanded_resp(h2b('af8023029000'))
|
||||
|
||||
|
||||
class ExpandedSmsPipelineTestCase(unittest.TestCase):
|
||||
"""expanded format + TS 102 225 SMS security witj 3DES keyset,
|
||||
to ensure remote_format does not affect the compact path"""
|
||||
def __init__(self, methodName='runTest', **kwargs):
|
||||
super().__init__(methodName, **kwargs)
|
||||
self.od = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=3,
|
||||
kic=h2b('C21DD66ACAC13CB3BC8B331B24AFB57B'),
|
||||
algo_auth='triple_des_cbc2', kid_idx=3,
|
||||
kid=h2b('12110C78E678C25408233076AA033615'))
|
||||
self.dialect = OtaDialectSms()
|
||||
self.tar = h2b('000000')
|
||||
|
||||
def test_cmd_expanded_secured_roundtrip(self):
|
||||
spi = SPI_CC_POR_CIPHERED_CC
|
||||
enc = self.dialect.encode_cmd(self.od, self.tar, spi, h2b('80f24002024f0000'),
|
||||
remote_format='expanded')
|
||||
# decode_cmd returns opaque 'Command Scripting template'
|
||||
dec_tar, dec_spi, dec_secured = self.dialect.decode_cmd(self.od, enc)
|
||||
self.assertEqual(b2h(dec_tar), b2h(self.tar))
|
||||
self.assertEqual(dec_spi, spi)
|
||||
self.assertEqual(b2h(dec_secured), 'aa0a220880f24002024f0000')
|
||||
|
||||
def test_cmd_expanded_list(self):
|
||||
spi = SPI_CC_POR_CIPHERED_CC
|
||||
enc = self.dialect.encode_cmd(self.od, self.tar, spi,
|
||||
[h2b('80f24002024f0000'), h2b('00a40004023f0000')],
|
||||
remote_format='expanded')
|
||||
_, _, dec_secured = self.dialect.decode_cmd(self.od, enc)
|
||||
parsed = ExpandedCmd.parse(dec_secured)
|
||||
self.assertEqual([c.c_apdu for c in parsed.commands],
|
||||
['80f24002024f0000', '00a40004023f0000'])
|
||||
|
||||
def test_resp_expanded_plaintext(self):
|
||||
# plaintext (u:nciphered + no CC) expanded response SMS
|
||||
# containing a 198 byte GP registry + SW 9000 as above, decode it through decode_resp().
|
||||
spi = SPI_CC_POR_UNCIPHERED_NOCC
|
||||
registry = bytes(range(198))
|
||||
secured = ExpandedRemoteResp.build(dict(body=dict(
|
||||
num_executed=dict(number_of_commands=1),
|
||||
responses=[dict(r_apdu=dict(response_data=b2h(registry), status_word='9000'))])))
|
||||
rpl = 1 + 3 + 5 + 1 + 1 + len(secured) # RHL-STS + secured data
|
||||
resp_body = rpl.to_bytes(2, 'big') + b'\x0a' + self.tar + b'\x00'*5 + b'\x00' + b'\x00' + secured
|
||||
sms = b'\x02\x71\x00' + resp_body
|
||||
r, dec = self.dialect.decode_resp(self.od, spi, sms, remote_format='expanded')
|
||||
self.assertEqual(r.response_status, 'por_ok')
|
||||
self.assertEqual(dec.number_of_commands, 1)
|
||||
self.assertEqual(dec.last_status_word, '9000')
|
||||
self.assertEqual(dec.last_response_data, b2h(registry))
|
||||
|
||||
def test_compact_still_default(self):
|
||||
# no remote_format -> compact default
|
||||
spi = SPI_CC_POR_UNCIPHERED_NOCC
|
||||
r, d = self.dialect.decode_resp(self.od, spi, '027100000e0ab000110000000000000001612f')
|
||||
self.assertEqual(d.number_of_commands, 1)
|
||||
self.assertEqual(d.last_status_word, '612f')
|
||||
self.assertEqual(d.last_response_data, '')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
""" test for smpp-ota-tool SMS handling, specifically the multi part sms OTA response"""
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Eric Wild <ewild@sysmocom.de>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os.path
|
||||
import importlib.util
|
||||
import unittest
|
||||
|
||||
from osmocom.utils import h2b, b2h
|
||||
|
||||
from pySim.ota import OtaKeyset, OtaDialectSms, ExpandedRemoteResp
|
||||
from pySim.sms import ConcatenatedSmsReassembler, UserDataHeader
|
||||
|
||||
# import the hyphenated contrib script as a module to get at SmppHandler
|
||||
# why do people name python files like that? why does everything have to be so hard?
|
||||
_TOOL_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'contrib', 'smpp-ota-tool.py')
|
||||
_spec = importlib.util.spec_from_file_location('smpp_ota_tool', _TOOL_PATH)
|
||||
smpp_ota_tool = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(smpp_ota_tool)
|
||||
SmppHandler = smpp_ota_tool.SmppHandler
|
||||
|
||||
|
||||
class _FakePdu:
|
||||
"""Minimal mock for smpplib deliver_sm pdu."""
|
||||
def __init__(self, short_message):
|
||||
self.short_message = short_message
|
||||
|
||||
|
||||
class MultipartRelayTestCase(unittest.TestCase):
|
||||
"""message_received_handler must return the reassembled application
|
||||
response and survive POR messages."""
|
||||
|
||||
# 3DES test keyset from tests/unittests/test_ota.py) used to make the
|
||||
# handler happy. responses are plaintext, tests do not depend on keys.
|
||||
def _handler(self, remote_format='expanded'):
|
||||
h = object.__new__(SmppHandler)
|
||||
h.client = None
|
||||
h.ota_dialect = OtaDialectSms()
|
||||
h.ota_keyset = OtaKeyset(algo_crypt='triple_des_cbc2', kic_idx=3,
|
||||
kic=h2b('C21DD66ACAC13CB3BC8B331B24AFB57B'),
|
||||
algo_auth='triple_des_cbc2', kid_idx=3,
|
||||
kid=h2b('12110C78E678C25408233076AA033615'))
|
||||
h.tar = h2b('000000')
|
||||
# unciphered, no CC, PoR required
|
||||
h.spi = {'counter': 'no_counter', 'ciphering': False, 'rc_cc_ds': 'no_rc_cc_ds',
|
||||
'por_in_submit': False, 'por': 'por_required',
|
||||
'por_shall_be_ciphered': False, 'por_rc_cc_ds': 'no_rc_cc_ds'}
|
||||
h.remote_format = remote_format
|
||||
h.reassembler = ConcatenatedSmsReassembler()
|
||||
h.response = None
|
||||
return h
|
||||
|
||||
@staticmethod
|
||||
def _plaintext_resp_sms(secured: bytes, sts: int = 0x00) -> bytes:
|
||||
"""Build a plaintext (unciphered, no-CC) OTA SMS response packet in the
|
||||
canonical single-part form (UDH 02 71 00 + response packet)."""
|
||||
rpl = 1 + 3 + 5 + 1 + 1 + len(secured) # RHL-STS + secured data
|
||||
body = (rpl.to_bytes(2, 'big') + b'\x0a' + h2b('000000') + b'\x00' * 5
|
||||
+ b'\x00' + bytes([sts]) + secured)
|
||||
return b'\x02\x71\x00' + body
|
||||
|
||||
@staticmethod
|
||||
def _expanded_secured(response_data_hex: str, sw: str = '9000') -> bytes:
|
||||
return ExpandedRemoteResp.build(dict(body=dict(
|
||||
num_executed=dict(number_of_commands=1),
|
||||
responses=[dict(r_apdu=dict(response_data=response_data_hex, status_word=sw))])))
|
||||
|
||||
@staticmethod
|
||||
def _fragment_2(tpud: bytes, ref: int, first_len: int):
|
||||
"""Split 02 71 00 + body TP-UD into two SMS parts:
|
||||
- part1 carries the OTA (0x71) IE
|
||||
- part2 only concatenat IE
|
||||
matches sja5 interaction"""
|
||||
assert tpud[:3] == b'\x02\x71\x00'
|
||||
body = tpud[3:]
|
||||
ota_ie = {'iei': 0x71, 'length': 0, 'value': b''}
|
||||
|
||||
def concat(seq):
|
||||
return {'iei': 0x00, 'length': 3, 'value': bytes([ref, 2, seq])}
|
||||
p1 = UserDataHeader([concat(1), ota_ie]).to_bytes() + body[:first_len]
|
||||
p2 = UserDataHeader([concat(2)]).to_bytes() + body[first_len:]
|
||||
return p1, p2
|
||||
|
||||
# ground truth: TP-User-Data captured from a sja5
|
||||
REAL_PART1 = h2b('070003010201710000e412000000df63afe4b06db21e2113be1be09e9b66f1c113ae841cca2d030064ec16b5b80ee5ce824604a4568109d25a82fb74a325df6f911bd0a4f858ece2c770039002c480269fc65953f5fd93ebbe528d97838bac4389a7303db2b073a37a9a1a51890457f41b49fc7905ce337e83449b65560501b8b845fe63339d557a928f2643')
|
||||
REAL_PART2 = h2b('050003010202fd9c4e50ec40fb4427af518e9c08697405d91fbb6e9fa0b0935f48a560e15f2f3f27a2e44ef3a47280acce77f030fb70eb3df863c159177e2c0e3e53052fc7bb7ed171a491ded3ab7921861176a04305bc09fcf526c07bf6bb48a19e67cf18be5bc1')
|
||||
REAL_REASSEMBLED = '02710000e412000000df63afe4b06db21e2113be1be09e9b66f1c113ae841cca2d030064ec16b5b80ee5ce824604a4568109d25a82fb74a325df6f911bd0a4f858ece2c770039002c480269fc65953f5fd93ebbe528d97838bac4389a7303db2b073a37a9a1a51890457f41b49fc7905ce337e83449b65560501b8b845fe63339d557a928f2643fd9c4e50ec40fb4427af518e9c08697405d91fbb6e9fa0b0935f48a560e15f2f3f27a2e44ef3a47280acce77f030fb70eb3df863c159177e2c0e3e53052fc7bb7ed171a491ded3ab7921861176a04305bc09fcf526c07bf6bb48a19e67cf18be5bc1'
|
||||
|
||||
def test_real_card_parts_reassemble(self):
|
||||
"""two real card TP-UDs recombine into 233-byte single part packet:
|
||||
UDH 02 71 00 + response packet"""
|
||||
r = ConcatenatedSmsReassembler()
|
||||
self.assertIsNone(r.add(self.REAL_PART1))
|
||||
out = r.add(self.REAL_PART2)
|
||||
self.assertEqual(len(out), 233)
|
||||
self.assertEqual(b2h(out), self.REAL_REASSEMBLED)
|
||||
|
||||
def test_multipart_response_not_overwritten_by_por(self):
|
||||
"""reassembled application response must survive the ENVELOPE
|
||||
trailing POR which contains no R-APDU"""
|
||||
registry = bytes(range(198))
|
||||
app = self._plaintext_resp_sms(self._expanded_secured(b2h(registry)))
|
||||
part1, part2 = self._fragment_2(app, ref=0x42, first_len=132)
|
||||
# single part form must be too fat -> both parts must be concatenated
|
||||
self.assertGreater(len(app), 140)
|
||||
# ENVELOPE PoR: por_ok, but no app R-APDU
|
||||
inline_por = self._plaintext_resp_sms(b'', sts=0x00)
|
||||
|
||||
h = self._handler()
|
||||
# arrival order
|
||||
self.assertIsNone(h.message_received_handler(_FakePdu(part1)))
|
||||
h.message_received_handler(_FakePdu(part2))
|
||||
h.message_received_handler(_FakePdu(inline_por))
|
||||
|
||||
# self.response must be app response, not the PoR!
|
||||
self.assertIsNotNone(h.response)
|
||||
res, decoded = h.response
|
||||
self.assertEqual(res.response_status, 'por_ok')
|
||||
self.assertIsNotNone(decoded)
|
||||
self.assertEqual(decoded.last_response_data, b2h(registry))
|
||||
self.assertEqual(decoded.last_status_word, '9000')
|
||||
|
||||
def test_undecodable_response_does_not_crash(self):
|
||||
"""response the handler can't decode must not escape out of the poll()
|
||||
loop which would kill the tool, it must be ignored"""
|
||||
# por_ok with a not expanded 'secured data' -> expanded parse raises
|
||||
bad = self._plaintext_resp_sms(h2b('01612f'), sts=0x00)
|
||||
h = self._handler(remote_format='expanded')
|
||||
# must NOT raise
|
||||
self.assertIsNone(h.message_received_handler(_FakePdu(bad)))
|
||||
self.assertIsNone(h.response)
|
||||
|
||||
def test_undecodable_por_after_good_response(self):
|
||||
"""real app response followed by undecodable PoR:
|
||||
- good response is saved
|
||||
- tool does not crash."""
|
||||
registry = bytes(range(120))
|
||||
app = self._plaintext_resp_sms(self._expanded_secured(b2h(registry)))
|
||||
part1, part2 = self._fragment_2(app, ref=0x07, first_len=110)
|
||||
bad_por = self._plaintext_resp_sms(h2b('deadbeef'), sts=0x00)
|
||||
|
||||
h = self._handler()
|
||||
h.message_received_handler(_FakePdu(part1))
|
||||
h.message_received_handler(_FakePdu(part2))
|
||||
self.assertIsNone(h.message_received_handler(_FakePdu(bad_por))) # no crash
|
||||
res, decoded = h.response
|
||||
self.assertIsNotNone(decoded)
|
||||
self.assertEqual(decoded.last_response_data, b2h(registry))
|
||||
|
||||
def test_single_part_response_still_works(self):
|
||||
"""small response that fits one SMS turns into self.response, handled as before"""
|
||||
h = self._handler()
|
||||
sms = self._plaintext_resp_sms(self._expanded_secured('abcd', sw='9000'))
|
||||
self.assertLessEqual(len(sms), 140)
|
||||
h.message_received_handler(_FakePdu(sms))
|
||||
res, decoded = h.response
|
||||
self.assertIsNotNone(decoded)
|
||||
self.assertEqual(decoded.last_response_data, 'abcd')
|
||||
self.assertEqual(decoded.last_status_word, '9000')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -103,126 +103,3 @@ class Test_DELIVER(unittest.TestCase):
|
||||
self.assertEqual(d.tp_pid, 0x7f)
|
||||
self.assertEqual(d.tp_dcs, 0xf6)
|
||||
self.assertEqual(d.tp_udl, 8)
|
||||
|
||||
|
||||
class Test_ConcatenatedSmsReassembler(unittest.TestCase):
|
||||
"""3GPP TS 23.040 9.2.3.24 reassembly of multi-part SMS.
|
||||
|
||||
An OTA response that exceeds a single SHORT MESSAGE is delivered in several parts using
|
||||
the SEND SHORT MESSAGE proactive command. The receiver must recombine the individual
|
||||
parts into a single part before decoding."""
|
||||
|
||||
OTA_IE = {'iei': 0x71, 'length': 0, 'value': b''}
|
||||
|
||||
@staticmethod
|
||||
def _concat8(ref, tot, seq):
|
||||
return {'iei': 0x00, 'length': 3, 'value': bytes([ref, tot, seq])}
|
||||
|
||||
@staticmethod
|
||||
def _concat16(ref, tot, seq):
|
||||
return {'iei': 0x08, 'length': 4, 'value': ref.to_bytes(2, 'big') + bytes([tot, seq])}
|
||||
|
||||
@staticmethod
|
||||
def _part(ies, frag):
|
||||
return UserDataHeader(ies).to_bytes() + frag
|
||||
|
||||
def test_ground_truth_udh(self):
|
||||
# part 1 UDH observed from sja5: 07 00 03 01 02 01 71 00
|
||||
built = self._part([self._concat8(1, 2, 1), self.OTA_IE], b'')
|
||||
self.assertEqual(b2h(built), '0700030102017100')
|
||||
|
||||
def test_ground_truth_udh_16bit(self):
|
||||
# 9.2.3.24.8: 08 | 08 04 <ref16> <total> <seq> | 71 00
|
||||
built = self._part([self._concat16(0x1234, 2, 1), self.OTA_IE], b'')
|
||||
self.assertEqual(b2h(built), '080804123402017100')
|
||||
|
||||
def test_single_part_passthrough(self):
|
||||
r = ConcatenatedSmsReassembler()
|
||||
single = h2b('027100') + bytes(range(20))
|
||||
self.assertEqual(r.add(single), single)
|
||||
|
||||
def test_two_part(self):
|
||||
# second segment contains only the concat IE, no OTA IE
|
||||
pkt = bytes(range(60))
|
||||
r = ConcatenatedSmsReassembler()
|
||||
self.assertIsNone(r.add(self._part([self._concat8(1, 2, 1), self.OTA_IE], pkt[:35])))
|
||||
out = r.add(self._part([self._concat8(1, 2, 2)], pkt[35:]))
|
||||
self.assertEqual(out, h2b('027100') + pkt)
|
||||
|
||||
def test_out_of_order(self):
|
||||
pkt = bytes(range(60))
|
||||
r = ConcatenatedSmsReassembler()
|
||||
self.assertIsNone(r.add(self._part([self._concat8(5, 2, 2), self.OTA_IE], pkt[35:])))
|
||||
out = r.add(self._part([self._concat8(5, 2, 1), self.OTA_IE], pkt[:35]))
|
||||
self.assertEqual(out, h2b('027100') + pkt)
|
||||
|
||||
def test_three_part_out_of_order(self):
|
||||
pkt = bytes(range(90))
|
||||
r = ConcatenatedSmsReassembler()
|
||||
self.assertIsNone(r.add(self._part([self._concat8(7, 3, 3)], pkt[60:])))
|
||||
self.assertIsNone(r.add(self._part([self._concat8(7, 3, 1), self.OTA_IE], pkt[:30])))
|
||||
out = r.add(self._part([self._concat8(7, 3, 2)], pkt[30:60]))
|
||||
self.assertEqual(out, h2b('027100') + pkt)
|
||||
|
||||
def test_16bit_reference(self):
|
||||
pkt = bytes(range(40))
|
||||
r = ConcatenatedSmsReassembler()
|
||||
self.assertIsNone(r.add(self._part([self._concat16(0x1234, 2, 1), self.OTA_IE], pkt[:20])))
|
||||
out = r.add(self._part([self._concat16(0x1234, 2, 2)], pkt[20:]))
|
||||
self.assertEqual(out, h2b('027100') + pkt)
|
||||
|
||||
def test_interleaved_references(self):
|
||||
# two concurrent concatenation sets at the same time
|
||||
pkt = bytes(range(60))
|
||||
r = ConcatenatedSmsReassembler()
|
||||
self.assertIsNone(r.add(self._part([self._concat8(1, 2, 1), self.OTA_IE], pkt[:35])))
|
||||
self.assertIsNone(r.add(self._part([self._concat8(9, 2, 1), self.OTA_IE], b'\xaa')))
|
||||
self.assertEqual(r.add(self._part([self._concat8(1, 2, 2)], pkt[35:])), h2b('027100') + pkt)
|
||||
self.assertEqual(r.add(self._part([self._concat8(9, 2, 2)], b'\xbb')), h2b('027100') + b'\xaa\xbb')
|
||||
|
||||
def test_reserved_concat_ie_is_ignored(self):
|
||||
# TS 23.040 9.2.3.24.1:
|
||||
# - a total of 0
|
||||
# - or a sequence number that is 0 or > total
|
||||
# means "the receiving entity shall ignore the whole Information Element"
|
||||
# the message is handed back unchanged as a single part msg and not rejected
|
||||
# so the caller can handle the problem
|
||||
r = ConcatenatedSmsReassembler()
|
||||
for tot, seq in [(2, 3), # seq > total
|
||||
(2, 0), # seq == 0
|
||||
(0, 1)]: # total == 0
|
||||
with self.subTest(total=tot, seq=seq):
|
||||
part = self._part([self._concat8(1, tot, seq)], b'\x00')
|
||||
self.assertEqual(r.add(part), part)
|
||||
# nothing buffered so later valid set still reassembles properly
|
||||
self.assertEqual(r.sets, {})
|
||||
pkt = bytes(range(40))
|
||||
self.assertIsNone(r.add(self._part([self._concat8(1, 2, 1), self.OTA_IE], pkt[:20])))
|
||||
self.assertEqual(r.add(self._part([self._concat8(1, 2, 2)], pkt[20:])), h2b('027100') + pkt)
|
||||
|
||||
def test_inconsistent_totals_do_not_crash(self):
|
||||
r = ConcatenatedSmsReassembler()
|
||||
self.assertIsNone(r.add(self._part([self._concat8(1, 3, 3)], b'\x33')))
|
||||
self.assertIsNone(r.add(self._part([self._concat8(1, 2, 1)], b'\x11')))
|
||||
self.assertEqual(r.add(self._part([self._concat8(1, 2, 2)], b'\x22')),
|
||||
h2b('00') + b'\x11\x22') # complete total=2 set
|
||||
self.assertIn((0x00, 1, 3), r.sets) # total=3 set still waits
|
||||
|
||||
def test_incomplete_sets_are_capped(self):
|
||||
r = ConcatenatedSmsReassembler(max_sets=2)
|
||||
for ref in (1, 2, 3):
|
||||
self.assertIsNone(r.add(self._part([self._concat8(ref, 2, 1)], bytes([ref]))))
|
||||
self.assertEqual(sorted(k[1] for k in r.sets), [2, 3]) # oldest evicted
|
||||
self.assertIsNone(r.add(self._part([self._concat8(1, 2, 2)], b'\x11')))
|
||||
self.assertEqual(sorted(k[1] for k in r.sets), [1, 3])
|
||||
self.assertEqual(r.add(self._part([self._concat8(3, 2, 2)], b'\x33')), h2b('00') + b'\x03\x33')
|
||||
|
||||
def test_same_reference_in_both_ie_forms(self):
|
||||
# the refno only unique per IE form (9.2.3.24.1 vs .8) -> two sets
|
||||
r = ConcatenatedSmsReassembler()
|
||||
self.assertIsNone(r.add(self._part([self._concat8(1, 2, 1)], b'\x0a')))
|
||||
self.assertIsNone(r.add(self._part([self._concat16(1, 2, 2)], b'\x1b')))
|
||||
self.assertEqual(r.add(self._part([self._concat16(1, 2, 1)], b'\x0b')),
|
||||
h2b('00') + b'\x0b\x1b')
|
||||
self.assertEqual(r.add(self._part([self._concat8(1, 2, 2)], b'\x1a')),
|
||||
h2b('00') + b'\x0a\x1a')
|
||||
|
||||
@@ -21,7 +21,6 @@ import logging
|
||||
from osmocom.utils import b2h, h2b, all_subclasses
|
||||
from osmocom.tlv import *
|
||||
|
||||
import pySim.cat
|
||||
import pySim.iso7816_4
|
||||
import pySim.ts_102_221
|
||||
import pySim.ts_102_222
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Transport (as in t0/t1) tests"""
|
||||
|
||||
# (C) 2026 by sysmocom - s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Author: Eric Wild <ewild@sysmocom.de>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import unittest
|
||||
from osmocom.utils import h2b, b2h
|
||||
from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result
|
||||
from pySim.transport import ProactiveHandler, LinkBaseTpdu
|
||||
|
||||
|
||||
def _send_short_message_pcmd():
|
||||
"""proactive SEND SHORT MESSAGE:
|
||||
D0 | CommandDetails(cmd 1, t 0x13, q 0) | DeviceIdentities(uicc->network)
|
||||
| dummy SMS_TPDU"""
|
||||
body = h2b('8103011300' + '82028183' + '8B04DEADBEEF')
|
||||
pdu = h2b('D0') + bytes([len(body)]) + body
|
||||
pcmd = ProactiveCommand()
|
||||
decoded = pcmd.from_tlv(pdu)
|
||||
return pcmd, decoded
|
||||
|
||||
|
||||
class Test_prepare_response(unittest.TestCase):
|
||||
"""TERMINAL RESPONSE.
|
||||
multi-part OTA response crash regression test."""
|
||||
|
||||
def setUp(self):
|
||||
self.h = ProactiveHandler.__new__(ProactiveHandler)
|
||||
|
||||
def test_on_decoded_command(self):
|
||||
_pcmd, decoded = _send_short_message_pcmd()
|
||||
til = self.h.prepare_response(decoded)
|
||||
self.assertEqual([type(c).__name__ for c in til],
|
||||
['CommandDetails', 'DeviceIdentities', 'Result'])
|
||||
# command details echoed, device id inverted, result OK
|
||||
self.assertEqual(b2h(til[0].to_tlv()), '8103011300')
|
||||
self.assertEqual(b2h(til[1].to_tlv()), '82028381')
|
||||
self.assertEqual(b2h(til[2].to_tlv()), '830100')
|
||||
|
||||
def test_on_collection_resolves_via_decoded(self):
|
||||
# Check that ProactiveCommand collection (empty .children) still works
|
||||
pcmd, _decoded = _send_short_message_pcmd()
|
||||
self.assertEqual(list(getattr(pcmd, 'children', []) or []), [])
|
||||
til = self.h.prepare_response(pcmd)
|
||||
self.assertEqual([type(c).__name__ for c in til],
|
||||
['CommandDetails', 'DeviceIdentities', 'Result'])
|
||||
self.assertEqual(b2h(til[0].to_tlv()), '8103011300')
|
||||
self.assertEqual(b2h(til[1].to_tlv()), '82028381')
|
||||
self.assertEqual(b2h(til[2].to_tlv()), '830100')
|
||||
|
||||
def test_missing_command_details_raises_clear_error(self):
|
||||
class _NoChildren:
|
||||
children = []
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self.h.prepare_response(_NoChildren())
|
||||
self.assertIn('CommandDetails', str(ctx.exception))
|
||||
|
||||
|
||||
class FakeTpduLink(LinkBaseTpdu):
|
||||
"""mock LinkBaseTpdu that replays a list of (data, sw) responses + records every TPDU that
|
||||
the T=0 state machine sends. Secretly sending more TPDUs than intended is the error,
|
||||
designed to test "unsolicited GET RESPONSE" mishaps"""
|
||||
|
||||
def __init__(self, responses):
|
||||
super().__init__()
|
||||
self._responses = list(responses)
|
||||
self.sent = []
|
||||
|
||||
def send_tpdu(self, tpdu):
|
||||
self.sent.append(tpdu.lower())
|
||||
if not self._responses:
|
||||
raise AssertionError('T=0 layer sent an unpexpected TPDU: %s (total so far: %s)'
|
||||
% (tpdu, self.sent))
|
||||
return self._responses.pop(0)
|
||||
|
||||
def __str__(self):
|
||||
return 'FakeTpduLink'
|
||||
|
||||
def wait_for_card(self, timeout=None, newcardonly=False):
|
||||
pass
|
||||
|
||||
def connect(self):
|
||||
pass
|
||||
|
||||
def get_atr(self):
|
||||
return '3b00'
|
||||
|
||||
def disconnect(self):
|
||||
pass
|
||||
|
||||
def _reset_card(self):
|
||||
pass
|
||||
|
||||
|
||||
# GP GET STATUS, wrapped in SCP02 CLA 84, Case #4.
|
||||
GET_STATUS = '84f22002094f005c054f9f70c5cc' + '00'
|
||||
GET_STATUS_TPDU = '84f22002094f005c054f9f70c5cc'
|
||||
|
||||
# generic #4 SELECT by DF name command
|
||||
CASE4 = '00a4040c07a0000000871002' + '00'
|
||||
CASE4_TPDU = '00a4040c07a0000000871002'
|
||||
|
||||
|
||||
class Test_send_apdu_T0(unittest.TestCase):
|
||||
"""regression tests for the T=0 state machine in LinkBaseTpdu.__send_apdu_T0()"""
|
||||
|
||||
def _exchange(self, apdu, responses, strict=True, protocol=0):
|
||||
link = FakeTpduLink(responses)
|
||||
link.apdu_strict = strict
|
||||
link.set_tpdu_format(protocol)
|
||||
data, sw = link._send_apdu(apdu)
|
||||
return link, data, sw
|
||||
|
||||
#### TS 102 221 section 7.3.1.1 TPDU construction
|
||||
|
||||
def test_case1_gets_le_appended(self):
|
||||
link, data, sw = self._exchange('00200001', [('', '9000')])
|
||||
self.assertEqual(link.sent, ['0020000100'])
|
||||
self.assertEqual((data, sw), ('', '9000'))
|
||||
|
||||
def test_case3_passed_through_unmodified(self):
|
||||
apdu = '00200001081122334455667788'
|
||||
link, _data, sw = self._exchange(apdu, [('', '9000')])
|
||||
self.assertEqual(link.sent, [apdu])
|
||||
self.assertEqual(sw, '9000')
|
||||
|
||||
def test_case4_le_stripped(self):
|
||||
link, data, sw = self._exchange(CASE4, [('', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU])
|
||||
self.assertEqual((data, sw), ('', '9000'))
|
||||
|
||||
#### TS 102 221 7.3.1.1.4 4a GP GET RESPONSE for 61xx / 9fxx
|
||||
|
||||
def test_61xx_fetches_response(self):
|
||||
link, data, sw = self._exchange(CASE4, [('', '6103'), ('a1b2c3', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000003'])
|
||||
self.assertEqual((data, sw), ('a1b2c3', '9000'))
|
||||
|
||||
def test_61xx_chained(self):
|
||||
link, data, sw = self._exchange(CASE4,
|
||||
[('', '6102'), ('aabb', '6102'), ('ccdd', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000002', '00c0000002'])
|
||||
self.assertEqual((data, sw), ('aabbccdd', '9000'))
|
||||
|
||||
def test_9fxx_fetches_response(self):
|
||||
link, data, sw = self._exchange(CASE4, [('', '9f04'), ('deadbeef', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000004'])
|
||||
self.assertEqual((data, sw), ('deadbeef', '9000'))
|
||||
|
||||
def test_get_response_inherits_cla(self):
|
||||
"""GET RESPONSE must reuse CLA of command"""
|
||||
link, _data, _sw = self._exchange(GET_STATUS, [('', '6102'), ('aabb', '9000')])
|
||||
self.assertEqual(link.sent, [GET_STATUS_TPDU, '84c0000002'])
|
||||
|
||||
def test_9100_terminates(self):
|
||||
"""9100 is final status word, not fetch trigger"""
|
||||
link, data, sw = self._exchange(CASE4, [('', '9100')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU])
|
||||
self.assertEqual((data, sw), ('', '9100'))
|
||||
|
||||
def test_error_sw_terminates(self):
|
||||
link, data, sw = self._exchange(CASE4, [('', '6982')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU])
|
||||
self.assertEqual((data, sw), ('', '6982'))
|
||||
|
||||
def test_no_status_word_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self._exchange(CASE4, [('', None)])
|
||||
|
||||
#### TS 102 221 7.3.1.1.4 4b dummy GET RESPONSE
|
||||
|
||||
def test_clause_4b_warning_before_data_bootstraps(self):
|
||||
"""warning SW returned for the _command_ TPDU triggers dummy GET RESPONSE (Le=00)"""
|
||||
for warn in ('6200', '6281', '62f1', '6300', '63f1'):
|
||||
with self.subTest(sw=warn):
|
||||
link, data, sw = self._exchange(CASE4,
|
||||
[('', warn), ('', '6103'), ('a1b2c3', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000000', '00c0000003'])
|
||||
self.assertEqual((data, sw), ('a1b2c3', '9000'))
|
||||
|
||||
def test_warning_after_data_terminates(self):
|
||||
"""Once the response has been fetched a warning status word is the final result of the command"""
|
||||
for warn in ('6281', '6283', '63c2', '6300', '62f1', '63f1', '6310'):
|
||||
with self.subTest(sw=warn):
|
||||
link, data, sw = self._exchange(CASE4, [('', '6102'), ('aabb', warn)])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000002'])
|
||||
self.assertEqual((data, sw), ('aabb', warn))
|
||||
|
||||
def test_no_dummy_get_response_when_command_already_returned_data(self):
|
||||
"""warning that arrives together with response data (for example 6282 on a case #2 read) is final, too"""
|
||||
link, data, sw = self._exchange('00b0000004', [('01020304', '6282')], strict=False)
|
||||
self.assertEqual(link.sent, ['00b0000004'])
|
||||
self.assertEqual((data, sw), ('01020304', '6282'))
|
||||
|
||||
def test_repeated_warning_does_not_loop(self):
|
||||
"""warning -> dummy GET RESPONSE -> warning again must terminate"""
|
||||
link, data, sw = self._exchange(CASE4, [('', '6281'), ('', '6281')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000000'])
|
||||
self.assertEqual((data, sw), ('', '6281'))
|
||||
|
||||
#### fixed GlobalPlatform GET STATUS pagination
|
||||
|
||||
def test_gp_6310_reaches_the_caller(self):
|
||||
"""GET STATUS answers 6310"""
|
||||
link, data, sw = self._exchange(GET_STATUS, [('', '6104'), ('e3024f00', '6310')])
|
||||
self.assertEqual(link.sent, [GET_STATUS_TPDU, '84c0000004'])
|
||||
self.assertEqual((data, sw), ('e3024f00', '6310'))
|
||||
|
||||
def test_gp_get_status_two_pages(self):
|
||||
"""Both GET STATUS pages, page 1 6310, reissued with P2 bit 1 set, page 2 9000."""
|
||||
page1 = self._exchange(GET_STATUS, [('', '6104'), ('e3024f00', '6310')])
|
||||
self.assertEqual(page1[1:], ('e3024f00', '6310'))
|
||||
page2 = self._exchange('84f22003094f005c054f9f70c5cc00',
|
||||
[('', '6104'), ('e3024f01', '9000')])
|
||||
self.assertEqual(page2[0].sent, ['84f22003094f005c054f9f70c5cc', '84c0000004'])
|
||||
self.assertEqual(page2[1:], ('e3024f01', '9000'))
|
||||
|
||||
#### 6cxx and apdu_strict
|
||||
|
||||
def test_6cxx_reissues_command_with_correct_length(self):
|
||||
link, data, sw = self._exchange('00b0000000', [('', '6c04'), ('01020304', '9000')])
|
||||
self.assertEqual(link.sent, ['00b0000000', '00b0000004'])
|
||||
self.assertEqual((data, sw), ('01020304', '9000'))
|
||||
|
||||
def test_strict_mode_does_not_auto_fetch_for_case3(self):
|
||||
apdu = '00200001081122334455667788'
|
||||
link, data, sw = self._exchange(apdu, [('', '6104')], strict=True)
|
||||
self.assertEqual(link.sent, [apdu])
|
||||
self.assertEqual((data, sw), ('', '6104'))
|
||||
|
||||
def test_non_strict_mode_auto_fetches_for_case3(self):
|
||||
apdu = '00200001081122334455667788'
|
||||
link, data, sw = self._exchange(apdu, [('', '6104'), ('aabbccdd', '9000')], strict=False)
|
||||
self.assertEqual(link.sent, [apdu, '00c0000004'])
|
||||
self.assertEqual((data, sw), ('aabbccdd', '9000'))
|
||||
|
||||
#### T=1 briefly
|
||||
|
||||
def test_t1_is_passed_through(self):
|
||||
"""T=1 has no GET RESPONSE"""
|
||||
link, data, sw = self._exchange(GET_STATUS, [('e3024f00', '6310')], protocol=1)
|
||||
self.assertEqual(link.sent, [GET_STATUS.lower()])
|
||||
self.assertEqual((data, sw), ('e3024f00', '6310'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -173,315 +173,315 @@ ok: TS48v5_SAIP2.1A_NoBERTLV.der MncLen(val= 3:int)
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp80Kvn01DesDek(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
@@ -873,315 +873,315 @@ ok: TS48v5_SAIP2.3_BERTLV_SUCI.der MncLen(val= 3:int)
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP02-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= 11020304050607080910111213141516:int)
|
||||
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= '010203040506070809101112131415161718192021222324':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
|
||||
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
read_back_val= {'SCP03-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||
|
||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp80Kvn01DesDek(val= '01020304050607080910111213141516':str)
|
||||
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||
|
||||
Reference in New Issue
Block a user