mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-10-04 19:57:53 +03:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d561aa6a2c | |||
| 2602017a60 | |||
| 3c437d41e0 | |||
| 2b7abdcf96 | |||
| aeba4004de | |||
| 632d585cfc | |||
| 7e8f711ec2 | |||
| d671cee649 | |||
| eb8e40948b | |||
| 0de8274e99 | |||
| df8de1a69a | |||
| 6b40fe8546 | |||
| 456c7873eb | |||
| 1b8c6b48ea | |||
| fd83fbdb5f | |||
| ff3f275c84 | |||
| a0e14a16f2 | |||
| 37719a0fcf | |||
| 26a3fc09dc | |||
| 515925228d | |||
| da94468c5f | |||
| 1c9072541b | |||
| e4e491ce58 | |||
| e70d9ec0c9 | |||
| 6076e4e6ff | |||
| 6313b83e0e | |||
| 5a54dd9eda | |||
| 63d4c447fb | |||
| 1e41568c12 | |||
| 2761d16582 | |||
| aeba4a547c | |||
| a8a94eae9c | |||
| 41e0d532f0 | |||
| e03530f89a | |||
| 078ac2bf19 | |||
| c582b5fee3 | |||
| d4717bd014 | |||
| 1cfb0f3da2 | |||
| cb3eb77236 | |||
| f381255639 | |||
| d13be84ccd | |||
| f4eb2f9356 |
@@ -1,2 +1,3 @@
|
|||||||
--exclude ^pySim/esim/asn1/.*\.asn$
|
--exclude ^pySim/esim/asn1/.*\.asn$
|
||||||
--exclude ^smdpp-data/.*$
|
--exclude ^smdpp-data/.*$
|
||||||
|
--exclude ^contrib/rcp/usage_example/certs/.*\.pem$
|
||||||
|
|||||||
Executable
+243
@@ -0,0 +1,243 @@
|
|||||||
|
#!/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)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
#!/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)
|
||||||
Executable
+666
@@ -0,0 +1,666 @@
|
|||||||
|
#!/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)
|
||||||
|
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
#!/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")
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
iccid,kic,kid,kik
|
||||||
|
8949440000001155306,F09C43EE1A0391665CC9F05AF4E0BD10,01981F4A20999F62AF99988007BAF6CA,8F8AEE5CDCC5D361368BC45673D99195
|
||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
"ICCID","KIC","KID","KIK"
|
||||||
|
"8949440000001155306","eae46224fa0a4ac1c12cba9d102f1188","3f14b978ddb38c08d832d4e4c2e0639d","9e19db4a5ed5cb8c4f5d96283eab273a"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-----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-----
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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-----
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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-----
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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-----
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/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
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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"
|
||||||
Executable
+130
@@ -0,0 +1,130 @@
|
|||||||
|
#!/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")
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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)
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/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 ""
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/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
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
. ./params.cfg
|
||||||
|
|
||||||
|
set -x
|
||||||
|
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
|
||||||
|
-h
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/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
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/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
|
||||||
Executable
+524
@@ -0,0 +1,524 @@
|
|||||||
|
#!/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()
|
||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/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())
|
||||||
+77
-19
@@ -24,7 +24,8 @@ import smpplib.gsm
|
|||||||
import smpplib.client
|
import smpplib.client
|
||||||
import smpplib.consts
|
import smpplib.consts
|
||||||
import time
|
import time
|
||||||
from pySim.ota import OtaKeyset, OtaDialectSms, OtaAlgoCrypt, OtaAlgoAuth, CNTR_REQ, RC_CC_DS, POR_REQ
|
from pySim.ota import OtaKeyset, OtaDialectSms, OtaAlgoCrypt, OtaAlgoAuth, OtaCheckError, CNTR_REQ, RC_CC_DS, POR_REQ
|
||||||
|
from pySim.sms import ConcatenatedSmsReassembler
|
||||||
from pySim.utils import b2h, h2b, is_hexstr
|
from pySim.utils import b2h, h2b, is_hexstr
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -70,6 +71,8 @@ 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('--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('--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('--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')
|
option_parser.add_argument('-a', '--apdu', action='append', required=True, type=is_hexstr, help='C-APDU to send')
|
||||||
|
|
||||||
class SmppHandler:
|
class SmppHandler:
|
||||||
@@ -77,7 +80,8 @@ class SmppHandler:
|
|||||||
|
|
||||||
def __init__(self, host: str, port: int,
|
def __init__(self, host: str, port: int,
|
||||||
system_id: str, password: str,
|
system_id: str, password: str,
|
||||||
ota_keyset: OtaKeyset, spi: dict, tar: bytes):
|
ota_keyset: OtaKeyset, spi: dict, tar: bytes,
|
||||||
|
remote_format: str = 'compact'):
|
||||||
"""
|
"""
|
||||||
Initialize connection to SMPP server and set static OTA SMS-TPDU ciphering parameters
|
Initialize connection to SMPP server and set static OTA SMS-TPDU ciphering parameters
|
||||||
Args:
|
Args:
|
||||||
@@ -88,6 +92,7 @@ class SmppHandler:
|
|||||||
ota_keyset: OTA keyset to be used for SMS-TPDU ciphering
|
ota_keyset: OTA keyset to be used for SMS-TPDU ciphering
|
||||||
spi: Security Parameter Indicator (SPI) 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
|
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
|
# Create and connect SMPP client
|
||||||
@@ -103,26 +108,58 @@ class SmppHandler:
|
|||||||
self.ota_keyset = ota_keyset
|
self.ota_keyset = ota_keyset
|
||||||
self.tar = tar
|
self.tar = tar
|
||||||
self.spi = spi
|
self.spi = spi
|
||||||
|
self.remote_format = remote_format
|
||||||
|
self.reassembler = ConcatenatedSmsReassembler()
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if self.client:
|
if self.client:
|
||||||
self.client.unbind()
|
self.client.unbind()
|
||||||
self.client.disconnect()
|
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):
|
def message_received_handler(self, pdu):
|
||||||
if pdu.short_message:
|
if not pdu.short_message:
|
||||||
logger.info("SMS-TPDU received: %s", b2h(pdu.short_message))
|
return None
|
||||||
try:
|
logger.info("SMS-TPDU received: %s", b2h(pdu.short_message))
|
||||||
dec = self.ota_dialect.decode_resp(self.ota_keyset, self.spi, pdu.short_message)
|
tpud = self.reassembler.add(pdu.short_message)
|
||||||
except ValueError:
|
if tpud is None:
|
||||||
# Retry to decoding with ciphering disabled (in case the card has problems to decode the SMS-TDPU
|
logger.info("SMS-TPDU is part of concat message, waiting for more parts...")
|
||||||
# we have sent, the response will contain an unencrypted error message)
|
return None
|
||||||
spi = self.spi.copy()
|
if tpud != pdu.short_message:
|
||||||
spi['por_shall_be_ciphered'] = False
|
logger.info("SMS-TPDU reassembled: %s", b2h(tpud))
|
||||||
spi['por_rc_cc_ds'] = 'no_rc_cc_ds'
|
try:
|
||||||
dec = self.ota_dialect.decode_resp(self.ota_keyset, spi, pdu.short_message)
|
res, decoded = self._decode_resp(tpud)
|
||||||
logger.info("SMS-TPDU decoded: %s", dec)
|
except Exception as e:
|
||||||
self.response = dec
|
# 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)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def message_sent_handler(self, pdu):
|
def message_sent_handler(self, pdu):
|
||||||
@@ -183,10 +220,14 @@ class SmppHandler:
|
|||||||
tuple containing the last response data and the last status word as byte strings
|
tuple containing the last response data and the last status word as byte strings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info("C-APDU sending: %s...", b2h(apdu))
|
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))
|
||||||
|
|
||||||
# translate to Secured OTA RFM
|
# translate to Secured OTA RFM
|
||||||
secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu)
|
secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu,
|
||||||
|
remote_format=self.remote_format)
|
||||||
# add user data header
|
# add user data header
|
||||||
tpdu = b'\x02\x70\x00' + secured
|
tpdu = b'\x02\x70\x00' + secured
|
||||||
# send via SMPP
|
# send via SMPP
|
||||||
@@ -200,6 +241,17 @@ class SmppHandler:
|
|||||||
container_dict = dict(container)
|
container_dict = dict(container)
|
||||||
resp = container_dict.get('last_response_data')
|
resp = container_dict.get('last_response_data')
|
||||||
sw = container_dict.get('last_status_word')
|
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:
|
if resp is None:
|
||||||
raise ValueError("Response does not contain any last_response_data, no R-APDU received!")
|
raise ValueError("Response does not contain any last_response_data, no R-APDU received!")
|
||||||
if sw is None:
|
if sw is None:
|
||||||
@@ -233,8 +285,14 @@ if __name__ == '__main__':
|
|||||||
'por_shall_be_ciphered': not opts.por_no_ciphering,
|
'por_shall_be_ciphered': not opts.por_no_ciphering,
|
||||||
'por_rc_cc_ds': opts.por_rc_cc_ds,
|
'por_rc_cc_ds': opts.por_rc_cc_ds,
|
||||||
'por': opts.por_req}
|
'por': opts.por_req}
|
||||||
apdu = h2b("".join(opts.apdu))
|
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))
|
||||||
|
|
||||||
smpp_handler = SmppHandler(opts.host, opts.port, opts.system_id, opts.password, ota_keyset, spi, h2b(opts.tar))
|
smpp_handler = SmppHandler(opts.host, opts.port, opts.system_id, opts.password, ota_keyset, spi,
|
||||||
|
h2b(opts.tar), remote_format=opts.format)
|
||||||
resp, sw = smpp_handler.transceive_apdu(apdu, opts.src_addr, opts.dest_addr, opts.timeout)
|
resp, sw = smpp_handler.transceive_apdu(apdu, opts.src_addr, opts.dest_addr, opts.timeout)
|
||||||
print("%s %s" % (b2h(resp), b2h(sw)))
|
print("%s %s" % (b2h(resp), b2h(sw)))
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
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('.')) # 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 -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
@@ -42,6 +44,7 @@ extensions = [
|
|||||||
"sphinx.ext.autosectionlabel",
|
"sphinx.ext.autosectionlabel",
|
||||||
"sphinx.ext.napoleon",
|
"sphinx.ext.napoleon",
|
||||||
"pysim_fs_sphinx",
|
"pysim_fs_sphinx",
|
||||||
|
"sphinx.ext.graphviz",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ pySim consists of several parts:
|
|||||||
suci-keytool
|
suci-keytool
|
||||||
saip-tool
|
saip-tool
|
||||||
smpp-ota-tool
|
smpp-ota-tool
|
||||||
|
rcpf
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|||||||
+690
@@ -0,0 +1,690 @@
|
|||||||
|
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,6 +170,35 @@ 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).
|
.. 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.
|
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
|
smpp-ota-tool syntax
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
+54
-3
@@ -136,6 +136,52 @@ from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subjec
|
|||||||
import logging # noqa: E402
|
import logging # noqa: E402
|
||||||
logger = logging.getLogger(__name__)
|
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
|
# HACK: make this configurable
|
||||||
DATA_DIR = './smdpp-data'
|
DATA_DIR = './smdpp-data'
|
||||||
HOSTNAME = 'testsmdpplus1.example.com' # must match certificates!
|
HOSTNAME = 'testsmdpplus1.example.com' # must match certificates!
|
||||||
@@ -479,7 +525,7 @@ class SmDppHttpServer:
|
|||||||
"""See ES9+ InitiateAuthentication SGP.22 Section 5.6.1"""
|
"""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
|
# 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".
|
# case-insensitive. Otherwise, the SM-DP+ SHALL return a status code "SM-DP+ Address - Refused".
|
||||||
if content['smdpAddress'] != self.server_hostname:
|
if content['smdpAddress'].lower() != self.server_hostname.lower():
|
||||||
raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address')
|
raise ApiError('8.8.1', '3.8', 'Invalid SM-DP+ Address')
|
||||||
|
|
||||||
euiccChallenge = b64decode(content['euiccChallenge'])
|
euiccChallenge = b64decode(content['euiccChallenge'])
|
||||||
@@ -870,12 +916,17 @@ def main(argv):
|
|||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
parser.add_argument("-m", "--in-memory", help="Use ephermal in-memory session storage (for concurrent runs)",
|
parser.add_argument("-m", "--in-memory", help="Use ephermal in-memory session storage (for concurrent runs)",
|
||||||
action='store_true', default=False)
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING)
|
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING)
|
||||||
|
|
||||||
common_cert_path = os.path.join(DATA_DIR, args.certdir)
|
common_cert_path = os.path.join(DATA_DIR, args.certdir)
|
||||||
hs = SmDppHttpServer(server_hostname=HOSTNAME, ci_certs_path=os.path.join(common_cert_path, 'CertificateIssuer'), common_cert_path=common_cert_path, use_brainpool=args.brainpool)
|
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)
|
||||||
if(args.nossl):
|
if(args.nossl):
|
||||||
hs.app.run(args.host, args.port)
|
hs.app.run(args.host, args.port)
|
||||||
else:
|
else:
|
||||||
@@ -904,7 +955,7 @@ def main(argv):
|
|||||||
with open(cert_pempath, 'wb') as pem_file:
|
with open(cert_pempath, 'wb') as pem_file:
|
||||||
pem_file.write(pem_cert)
|
pem_file.write(pem_cert)
|
||||||
|
|
||||||
SERVER_STRING = f'ssl:{args.port}:privateKey={cert_skpath}:certKey={cert_pempath}:dhParameters={dhparam_path}'
|
SERVER_STRING = f'ssl:{args.port}:privateKey={cert_skpath}:certKey={cert_pempath}:dhParameters={dhparam_path}:interface={args.host}'
|
||||||
print(SERVER_STRING)
|
print(SERVER_STRING)
|
||||||
|
|
||||||
hs.app.run(host=HOSTNAME, port=args.port, endpoint_description=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("")
|
||||||
print("Card programming failed with an exception:")
|
print("Card programming failed with an exception:")
|
||||||
print("---------------------8<---------------------")
|
print("---------------------8<---------------------")
|
||||||
traceback.print_exc()
|
print(traceback.format_exc().rstrip())
|
||||||
print("---------------------8<---------------------")
|
print("---------------------8<---------------------")
|
||||||
print("")
|
print("")
|
||||||
rc = -1
|
rc = -1
|
||||||
|
|||||||
+21
-19
@@ -101,7 +101,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
|||||||
self.numeric_path = False
|
self.numeric_path = False
|
||||||
self.conserve_write = True
|
self.conserve_write = True
|
||||||
self.json_pretty_print = True
|
self.json_pretty_print = True
|
||||||
self.apdu_trace = False
|
self.apdu_trace = getattr(sl, 'apdu_tracer', None) is not None
|
||||||
self.apdu_strict = False
|
self.apdu_strict = False
|
||||||
|
|
||||||
self.add_settable(cmd2.Settable('numeric_path', bool,
|
self.add_settable(cmd2.Settable('numeric_path', bool,
|
||||||
@@ -210,8 +210,10 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
|||||||
def __init__(self, cmd2_app):
|
def __init__(self, cmd2_app):
|
||||||
self.cmd2 = cmd2_app
|
self.cmd2 = cmd2_app
|
||||||
|
|
||||||
def trace_response(self, cmd, sw, resp):
|
def trace_command(self, cmd):
|
||||||
self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
|
self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
|
||||||
|
|
||||||
|
def trace_response(self, cmd, sw, resp):
|
||||||
self.cmd2.poutput("<- %s: %s" % (sw, resp))
|
self.cmd2.poutput("<- %s: %s" % (sw, resp))
|
||||||
|
|
||||||
def update_prompt(self):
|
def update_prompt(self):
|
||||||
@@ -349,7 +351,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
|||||||
self.poutput("")
|
self.poutput("")
|
||||||
self.poutput("Card initialization (%s) failed with an exception:" % str(self.sl))
|
self.poutput("Card initialization (%s) failed with an exception:" % str(self.sl))
|
||||||
self.poutput("---------------------8<---------------------")
|
self.poutput("---------------------8<---------------------")
|
||||||
traceback.print_exc()
|
self.poutput(traceback.format_exc().rstrip())
|
||||||
self.poutput("---------------------8<---------------------")
|
self.poutput("---------------------8<---------------------")
|
||||||
self.poutput("")
|
self.poutput("")
|
||||||
return -1
|
return -1
|
||||||
@@ -463,7 +465,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
|||||||
self.poutput("")
|
self.poutput("")
|
||||||
self.poutput("Card handling (%s) failed with an exception:" % str(self.sl))
|
self.poutput("Card handling (%s) failed with an exception:" % str(self.sl))
|
||||||
self.poutput("---------------------8<---------------------")
|
self.poutput("---------------------8<---------------------")
|
||||||
traceback.print_exc()
|
self.poutput(traceback.format_exc().rstrip())
|
||||||
self.poutput("---------------------8<---------------------")
|
self.poutput("---------------------8<---------------------")
|
||||||
self.poutput("")
|
self.poutput("")
|
||||||
fail_count = fail_count + 1
|
fail_count = fail_count + 1
|
||||||
@@ -1129,10 +1131,13 @@ global_group.add_argument("--verbose", help="Enable verbose logging",
|
|||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
|
|
||||||
adm_group = global_group.add_mutually_exclusive_group()
|
adm_group = global_group.add_mutually_exclusive_group()
|
||||||
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
|
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM', dest='pin_adm', default=None,
|
||||||
help='ADM PIN used for provisioning (overwrites default)')
|
help='ADM PIN used for provisioning (overwrites default)')
|
||||||
adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
|
adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM_HEX', dest='pin_adm_hex', default=None,
|
||||||
help='ADM PIN used for provisioning, as hex string (16 characters long)')
|
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=[],
|
option_parser.add_argument('-e', '--execute-command', action='append', default=[],
|
||||||
help='A pySim-shell command that will be executed at startup')
|
help='A pySim-shell command that will be executed at startup')
|
||||||
@@ -1171,7 +1176,7 @@ if __name__ == '__main__':
|
|||||||
startup_errors = True
|
startup_errors = True
|
||||||
print("Card initialization (%s) failed with an exception:" % str(sl))
|
print("Card initialization (%s) failed with an exception:" % str(sl))
|
||||||
print("---------------------8<---------------------")
|
print("---------------------8<---------------------")
|
||||||
traceback.print_exc()
|
print(traceback.format_exc().rstrip())
|
||||||
print("---------------------8<---------------------")
|
print("---------------------8<---------------------")
|
||||||
if not opts.noprompt:
|
if not opts.noprompt:
|
||||||
print("(you may still try to recover from this manually by using the 'equip' command.)")
|
print("(you may still try to recover from this manually by using the 'equip' command.)")
|
||||||
@@ -1182,18 +1187,15 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# If the user supplies an ADM PIN at via commandline args authenticate
|
# 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
|
# immediately so that the user does not have to use the shell commands
|
||||||
pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
|
pin_adm_type = ""
|
||||||
if pin_adm:
|
if opts.pin_adm_type:
|
||||||
if not card:
|
pin_adm_type = "--adm-type %s" % opts.pin_adm_type
|
||||||
print("Card error, cannot do ADM verification with supplied ADM pin now.")
|
if opts.pin_adm:
|
||||||
try:
|
app.onecmd_plus_hooks("verify_adm %s %s" %
|
||||||
card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
|
(opts.pin_adm, pin_adm_type), add_to_history = False)
|
||||||
except Exception as e:
|
elif opts.pin_adm_hex:
|
||||||
startup_errors = True
|
app.onecmd_plus_hooks("verify_adm %s --pin-is-hex %s" %
|
||||||
print("ADM verification (%s) failed with an exception:" % str(pin_adm))
|
(opts.pin_adm_hex, pin_adm_type), add_to_history = False)
|
||||||
print("---------------------8<---------------------")
|
|
||||||
print(e)
|
|
||||||
print("---------------------8<---------------------")
|
|
||||||
|
|
||||||
# Run optional commands
|
# Run optional commands
|
||||||
for c in opts.execute_command:
|
for c in opts.execute_command:
|
||||||
|
|||||||
+33
-227
@@ -30,10 +30,13 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
import colorlog
|
import colorlog
|
||||||
|
|
||||||
from twisted.protocols import basic
|
from twisted.protocols import basic
|
||||||
from twisted.internet import defer, endpoints, protocol, reactor, task
|
from twisted.internet import defer, endpoints, reactor, task
|
||||||
from twisted.cred.portal import IRealm
|
from twisted.cred.portal import IRealm
|
||||||
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
|
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
|
||||||
from twisted.cred.portal import Portal
|
from twisted.cred.portal import Portal
|
||||||
@@ -47,13 +50,16 @@ from smpp.pdu import pdu_types, operations, pdu_encoding
|
|||||||
|
|
||||||
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
|
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.transport import LinkBase, ProactiveHandler, argparse_add_reader_args, init_reader, ApduTracer
|
||||||
from pySim.commands import SimCardCommands
|
from pySim.commands import SimCardCommands
|
||||||
from pySim.cards import UiccCardBase
|
from pySim.cards import UiccCardBase
|
||||||
from pySim.exceptions import *
|
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 ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload, BearerDescription
|
||||||
from pySim.cat import DeviceIdentities, Address, OtherAddress, UiccTransportLevel, BufferSize
|
from pySim.cat import DeviceIdentities, Address, OtherAddress, UiccTransportLevel, BufferSize
|
||||||
from pySim.cat import ChannelStatus, ChannelData, ChannelDataLength
|
from pySim.cat import ChannelStatus, ChannelData, ChannelDataLength
|
||||||
|
from pySim.cat import EventList, EventDownload, Result
|
||||||
from pySim.utils import b2h, h2b
|
from pySim.utils import b2h, h2b
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -71,224 +77,6 @@ class MyApduTracer(ApduTracer):
|
|||||||
print("-> %s %s" % (cmd[:10], cmd[10:]))
|
print("-> %s %s" % (cmd[:10], cmd[10:]))
|
||||||
print("<- %s: %s" % (sw, resp))
|
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):
|
def dcs_is_8bit(dcs):
|
||||||
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
||||||
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
|
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
|
||||||
@@ -323,6 +111,11 @@ class MyServer:
|
|||||||
smppEndpoint = endpoints.TCP6ServerEndpoint(reactor, tcp_port, interface=bind_ip)
|
smppEndpoint = endpoints.TCP6ServerEndpoint(reactor, tcp_port, interface=bind_ip)
|
||||||
smppEndpoint.listen(self.factory)
|
smppEndpoint.listen(self.factory)
|
||||||
self.tp = self.scc = self.card = None
|
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):
|
def connect_to_card(self, tp: LinkBase):
|
||||||
self.tp = tp
|
self.tp = tp
|
||||||
@@ -333,8 +126,22 @@ class MyServer:
|
|||||||
self.scc.sel_ctrl = "0004"
|
self.scc.sel_ctrl = "0004"
|
||||||
self.card.read_aids()
|
self.card.read_aids()
|
||||||
self.card.select_adf_by_aid(adf='usim')
|
self.card.select_adf_by_aid(adf='usim')
|
||||||
# FIXME: create a more realistic profile than ffffff
|
self.scc.terminal_profile(b2h(terminal_profile()))
|
||||||
self.scc.terminal_profile('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
|
# 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))
|
||||||
|
|
||||||
def _msgHandler(self, system_id, smpp, pdu):
|
def _msgHandler(self, system_id, smpp, pdu):
|
||||||
"""Handler for incoming messages received via SMPP from ESME."""
|
"""Handler for incoming messages received via SMPP from ESME."""
|
||||||
@@ -362,14 +169,12 @@ class MyServer:
|
|||||||
tpdu = SMS_DELIVER.from_smpp_submit(pdu)
|
tpdu = SMS_DELIVER.from_smpp_submit(pdu)
|
||||||
logger.info(tpdu)
|
logger.info(tpdu)
|
||||||
# 2) wrap into the CAT ENVELOPE for SMS-PP-Download
|
# 2) wrap into the CAT ENVELOPE for SMS-PP-Download
|
||||||
tpdu_ie = SMS_TPDU(decoded={'tpdu': b2h(tpdu.to_bytes())})
|
sms_dl = sms_pp_download_envelope(tpdu)
|
||||||
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
|
# 3) send to the card
|
||||||
envelope_hex = b2h(sms_dl.to_tlv())
|
envelope_hex = b2h(sms_dl.to_tlv())
|
||||||
logger.info("ENVELOPE: %s" % envelope_hex)
|
logger.info("ENVELOPE: %s" % envelope_hex)
|
||||||
(data, sw) = self.scc.envelope(envelope_hex)
|
with self._card_lock:
|
||||||
|
(data, sw) = self.scc.envelope(envelope_hex)
|
||||||
logger.info("SW %s: %s" % (sw, data))
|
logger.info("SW %s: %s" % (sw, data))
|
||||||
if sw in ['9200', '9300']:
|
if sw in ['9200', '9300']:
|
||||||
# TODO send back RP-ERROR message with TP-FCS == 'SIM Application Toolkit Busy'
|
# TODO send back RP-ERROR message with TP-FCS == 'SIM Application Toolkit Busy'
|
||||||
@@ -416,7 +221,8 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
opts = option_parser.parse_args()
|
opts = option_parser.parse_args()
|
||||||
|
|
||||||
tp = init_reader(opts, proactive_handler = Proact())
|
tp = init_reader(opts, proactive_handler = Proact(
|
||||||
|
sms_sink=lambda pdu: hackish_global_smpp.sendDataRequest(pdu)))
|
||||||
if tp is None:
|
if tp is None:
|
||||||
exit(1)
|
exit(1)
|
||||||
tp.connect()
|
tp.connect()
|
||||||
|
|||||||
+4
-3
@@ -30,10 +30,11 @@ from pySim.log import PySimLogger
|
|||||||
|
|
||||||
log = PySimLogger.get(__name__)
|
log = PySimLogger.get(__name__)
|
||||||
|
|
||||||
# we need to import this module so that the SysmocomSJA2 sub-class of
|
# we need to import these modules so that the SysmocomSJA2 / SysmocomSJS1
|
||||||
# CardModel is created, which will add the ATR-based matching and
|
# sub-classes of CardModel are created, which will add the ATR-based matching
|
||||||
# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
|
# and calling of their add_files. See CardModel.apply_matching_models
|
||||||
import pySim.sysmocom_sja2
|
import pySim.sysmocom_sja2
|
||||||
|
import pySim.sysmocom_sjs1
|
||||||
|
|
||||||
# we need to import these modules so that the various sub-classes of
|
# 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
|
# CardProfile are created, which will be used in init_card() to iterate
|
||||||
|
|||||||
+627
@@ -0,0 +1,627 @@
|
|||||||
|
# -*- 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+83
-8
@@ -22,7 +22,7 @@ from typing import List
|
|||||||
from bidict import bidict
|
from bidict import bidict
|
||||||
from construct import Int8ub, Int16ub, Byte, BitsInteger
|
from construct import Int8ub, Int16ub, Byte, BitsInteger
|
||||||
from construct import Struct, Enum, BitStruct, this
|
from construct import Struct, Enum, BitStruct, this
|
||||||
from construct import Switch, GreedyRange, FlagsEnum
|
from construct import Switch, GreedyRange, FlagsEnum, Adapter
|
||||||
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
|
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
|
||||||
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
|
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
|
||||||
from osmocom.utils import b2h, h2b
|
from osmocom.utils import b2h, h2b
|
||||||
@@ -318,11 +318,58 @@ class FileList(COMPR_TLV_IE, tag=0x92):
|
|||||||
|
|
||||||
# TS 102 223 Section 8.19
|
# TS 102 223 Section 8.19
|
||||||
class LocationInformation(COMPR_TLV_IE, tag=0x93):
|
class LocationInformation(COMPR_TLV_IE, tag=0x93):
|
||||||
pass
|
# 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
|
||||||
|
|
||||||
# TS 102 223 Section 8.20
|
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.
|
||||||
class IMEI(COMPR_TLV_IE, tag=0x94):
|
class IMEI(COMPR_TLV_IE, tag=0x94):
|
||||||
_construct = BcdAdapter(GreedyBytes)
|
_test_de_encode = [
|
||||||
|
( '94081a32547698103254', '123456789012345' ),
|
||||||
|
]
|
||||||
|
_construct = MobileIdentityAdapter(GreedyBytes, MobileIdentityAdapter.TYPE_IMEI)
|
||||||
|
|
||||||
# TS 102 223 Section 8.21
|
# TS 102 223 Section 8.21
|
||||||
class HelpRequest(COMPR_TLV_IE, tag=0x95):
|
class HelpRequest(COMPR_TLV_IE, tag=0x95):
|
||||||
@@ -536,9 +583,9 @@ class Aid(COMPR_TLV_IE, tag=0xAF):
|
|||||||
|
|
||||||
# TS 102 223 Section 8.61
|
# TS 102 223 Section 8.61
|
||||||
class AccessTechnology(COMPR_TLV_IE, tag=0xBF):
|
class AccessTechnology(COMPR_TLV_IE, tag=0xBF):
|
||||||
SingleAccessTech = Enum(Int8ub, gsm=0, tia_eia_533=1, tia_eia_136_270=2, utran=3, tetra=4,
|
SingleAccessTech = Enum(Int8ub, gsm=0, tia_eia_553=1, tia_eia_136_270=2, utran=3, tetra=4,
|
||||||
tia_eia_95_b=5, cdma1000_1x=6, cdma2000_hrpd=7, eutran=8,
|
tia_eia_95_b=5, cdma2000_1x=6, cdma2000_hrpd=7, eutran=8,
|
||||||
ehrpd=9, nr=0x0a)
|
ehrpd=9, nr=0x0a, satellite_nr=0x0b, satellite_eutran=0x0c)
|
||||||
_construct = GreedyRange(SingleAccessTech)
|
_construct = GreedyRange(SingleAccessTech)
|
||||||
|
|
||||||
# TS 102 223 Section 8.63
|
# TS 102 223 Section 8.63
|
||||||
@@ -596,6 +643,14 @@ class UtranEutranMeasurementQualifier(COMPR_TLV_IE, tag=0xE9):
|
|||||||
eutran_inter_rat_utran=0x08,
|
eutran_inter_rat_utran=0x08,
|
||||||
eutran_inter_rat_nr=0x09)
|
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
|
# TS 102 223 Section 8.75
|
||||||
class NetworkSearchMode(COMPR_TLV_IE, tag=0xE5):
|
class NetworkSearchMode(COMPR_TLV_IE, tag=0xE5):
|
||||||
_construct = Enum(Int8ub, manual=0, automatic=1)
|
_construct = Enum(Int8ub, manual=0, automatic=1)
|
||||||
@@ -729,8 +784,12 @@ class DnsServerAddress(COMPR_TLV_IE, tag=0xC0):
|
|||||||
|
|
||||||
# TS 102 223 Section 8.105
|
# TS 102 223 Section 8.105
|
||||||
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
||||||
|
# 2 bytes/entry:
|
||||||
|
# - technology of 8.61
|
||||||
|
# - state byte b1 is 0 disabled/1 enabled
|
||||||
|
# - b2-b8 RFU.
|
||||||
AccessTechTuple = Struct('technology'/AccessTechnology.SingleAccessTech,
|
AccessTechTuple = Struct('technology'/AccessTechnology.SingleAccessTech,
|
||||||
'state'/FlagsEnum(Int8ub, enabled=0))
|
'state'/FlagsEnum(Int8ub, enabled=1))
|
||||||
_construct = GreedyRange(AccessTechTuple)
|
_construct = GreedyRange(AccessTechTuple)
|
||||||
|
|
||||||
# TS 102 223 Section 8.107
|
# TS 102 223 Section 8.107
|
||||||
@@ -763,6 +822,22 @@ class SMSPPDownload(BER_TLV_IE, tag=0xD1,
|
|||||||
nested=[DeviceIdentities, Address, SMS_TPDU]):
|
nested=[DeviceIdentities, Address, SMS_TPDU]):
|
||||||
pass
|
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
|
# TS 101 220 Table 7.17 + 31.111 7.1.1.3
|
||||||
class SMSCBDownload(BER_TLV_IE, tag=0xD2,
|
class SMSCBDownload(BER_TLV_IE, tag=0xD2,
|
||||||
nested=[DeviceIdentities, CBSPage]):
|
nested=[DeviceIdentities, CBSPage]):
|
||||||
|
|||||||
@@ -873,30 +873,6 @@ class SdKey(BinaryParam):
|
|||||||
|
|
||||||
SdKey.all_implementations = []
|
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):
|
def camel(s):
|
||||||
return s[:1].upper() + s[1:].lower()
|
return s[:1].upper() + s[1:].lower()
|
||||||
|
|
||||||
@@ -928,8 +904,6 @@ class SdKey(BinaryParam):
|
|||||||
|
|
||||||
max_key_len = attrs.get('allow_len')[-1]
|
max_key_len = attrs.get('allow_len')[-1]
|
||||||
|
|
||||||
cls_label = transitional_name_mapping.get(cls_label, cls_label)
|
|
||||||
|
|
||||||
attrs.update({
|
attrs.update({
|
||||||
'name' : cls_label,
|
'name' : cls_label,
|
||||||
'kvn': kvn,
|
'kvn': kvn,
|
||||||
|
|||||||
+4
-3
@@ -38,15 +38,16 @@ class SwMatchError(Exception):
|
|||||||
"""Raised when an operation specifies an expected SW but the actual SW from
|
"""Raised when an operation specifies an expected SW but the actual SW from
|
||||||
the card doesn't match."""
|
the card doesn't match."""
|
||||||
|
|
||||||
def __init__(self, sw_actual: str, sw_expected: str, rs=None):
|
def __init__(self, sw_actual: str, sw_expected, rs=None):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
sw_actual : the SW we actually received from the card (4 hex digits)
|
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)
|
sw_expected : the SW we expected to receive from the card (4 hex digits),
|
||||||
|
or a list of acceptable ones
|
||||||
rs : interpreter class to convert SW to string
|
rs : interpreter class to convert SW to string
|
||||||
"""
|
"""
|
||||||
self.sw_actual = sw_actual
|
self.sw_actual = sw_actual
|
||||||
self.sw_expected = sw_expected
|
self.sw_expected = '/'.join(sw_expected) if isinstance(sw_expected, (list, tuple)) else sw_expected
|
||||||
self.rs = rs
|
self.rs = rs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
+360
-128
@@ -18,10 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import hashlib
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Optional, List, Dict, Tuple
|
from typing import Optional, List, Dict, Tuple
|
||||||
from construct import Optional as COptional
|
from construct import Optional as COptional
|
||||||
from construct import Struct, GreedyRange, FlagsEnum, Int16ub, Int24ub, Padding, Bit, Const
|
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.Random import get_random_bytes
|
||||||
from Cryptodome.Cipher import DES, DES3, AES
|
from Cryptodome.Cipher import DES, DES3, AES
|
||||||
from osmocom.utils import *
|
from osmocom.utils import *
|
||||||
@@ -29,12 +31,15 @@ from osmocom.tlv import *
|
|||||||
from osmocom.construct import *
|
from osmocom.construct import *
|
||||||
from pySim.utils import ResTuple
|
from pySim.utils import ResTuple
|
||||||
from pySim.card_key_provider import card_key_provider_get_field
|
from pySim.card_key_provider import card_key_provider_get_field
|
||||||
from pySim.global_platform.scp import SCP02, SCP03
|
from pySim.global_platform.scp import SCP, SCP02, SCP03
|
||||||
from pySim.global_platform.install_param import gen_install_parameters
|
from pySim.global_platform.install_param import gen_install_parameters
|
||||||
from pySim.filesystem import *
|
from pySim.filesystem import *
|
||||||
from pySim.profile import CardProfile
|
from pySim.profile import CardProfile
|
||||||
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
|
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
|
||||||
from pySim.javacard import CapFile
|
from pySim.javacard import CapFile
|
||||||
|
from pySim.log import PySimLogger
|
||||||
|
|
||||||
|
log = PySimLogger.get(__name__)
|
||||||
|
|
||||||
# GPCS Table 11-48 Load Parameter Tags
|
# GPCS Table 11-48 Load Parameter Tags
|
||||||
class NonVolatileCodeMinMemoryReq(BER_TLV_IE, tag=0xC6):
|
class NonVolatileCodeMinMemoryReq(BER_TLV_IE, tag=0xC6):
|
||||||
@@ -148,6 +153,24 @@ 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
|
# GlobalPlatform 2.1.1 Section 9.1.6
|
||||||
KeyType = Enum(Byte, des=0x80,
|
KeyType = Enum(Byte, des=0x80,
|
||||||
tls_psk=0x85, # v2.3.1 Section 11.1.8
|
tls_psk=0x85, # v2.3.1 Section 11.1.8
|
||||||
@@ -512,6 +535,63 @@ class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCy
|
|||||||
ExecutableModuleAID, AssociatedSecurityDomainAID]):
|
ExecutableModuleAID, AssociatedSecurityDomainAID]):
|
||||||
pass
|
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
|
# Application Dedicated File of a Security Domain
|
||||||
class ADF_SD(CardADF):
|
class ADF_SD(CardADF):
|
||||||
StoreData = BitStruct('last_block'/Flag,
|
StoreData = BitStruct('last_block'/Flag,
|
||||||
@@ -527,6 +607,241 @@ class ADF_SD(CardADF):
|
|||||||
def decode_select_response(self, data_hex: str) -> object:
|
def decode_select_response(self, data_hex: str) -> object:
|
||||||
return decode_select_response(data_hex)
|
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')
|
@with_default_category('Application-Specific Commands')
|
||||||
class AddlShellCommands(CommandSet):
|
class AddlShellCommands(CommandSet):
|
||||||
get_data_parser = argparse.ArgumentParser()
|
get_data_parser = argparse.ArgumentParser()
|
||||||
@@ -544,7 +859,8 @@ class ADF_SD(CardADF):
|
|||||||
self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name,
|
self._cmd.poutput('Unknown data object "%s", available options: %s' % (tlv_cls_name,
|
||||||
do_names))
|
do_names))
|
||||||
return
|
return
|
||||||
(data, _sw) = self._cmd.lchan.scc.get_data(cla=0x80, tag=tlv_cls.tag)
|
|
||||||
|
data = ADF_SD.get_data(self._cmd.lchan.scc, tag=tlv_cls.tag)
|
||||||
ie = tlv_cls()
|
ie = tlv_cls()
|
||||||
ie.from_tlv(h2b(data))
|
ie.from_tlv(h2b(data))
|
||||||
self._cmd.poutput_json(ie.to_dict())
|
self._cmd.poutput_json(ie.to_dict())
|
||||||
@@ -565,27 +881,8 @@ class ADF_SD(CardADF):
|
|||||||
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
|
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
|
||||||
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
|
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
|
||||||
response_permitted = opts.response == 'may_be_returned'
|
response_permitted = opts.response == 'may_be_returned'
|
||||||
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
|
ADF_SD.store_data(self._cmd.lchan.scc, 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 = argparse.ArgumentParser()
|
||||||
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')
|
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')
|
||||||
@@ -602,8 +899,8 @@ class ADF_SD(CardADF):
|
|||||||
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.
|
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
|
The KCV (Key Check Values) can either be explicitly specified using `--key-check`, or will
|
||||||
otherwise be automatically generated for DES and AES keys. You can suppress the latter using
|
otherwise be automatically generated for DES, AES and TLS-PSK keys. You can suppress the
|
||||||
`--suppress-key-check`.
|
latter using `--suppress-key-check`.
|
||||||
|
|
||||||
Example (SCP80 KIC/KID/KIK):
|
Example (SCP80 KIC/KID/KIK):
|
||||||
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
|
put_key --key-version-nr 1 --key-id 0x01 --key-type aes --key-data 000102030405060708090a0b0c0d0e0f
|
||||||
@@ -620,36 +917,17 @@ class ADF_SD(CardADF):
|
|||||||
kdb = []
|
kdb = []
|
||||||
for i in range(0, len(opts.key_type)):
|
for i in range(0, len(opts.key_type)):
|
||||||
if opts.key_check and len(opts.key_check) > i:
|
if opts.key_check and len(opts.key_check) > i:
|
||||||
kcv = opts.key_check[i]
|
kcv = h2b(opts.key_check[i])
|
||||||
elif opts.suppress_key_check:
|
elif opts.suppress_key_check:
|
||||||
kcv = ''
|
kcv = b''
|
||||||
else:
|
else:
|
||||||
kcv_bin = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
|
kcv = compute_kcv(opts.key_type[i], h2b(opts.key_data[i])) or b''
|
||||||
kcv = b2h(kcv_bin)
|
kdb.append({'key_type': opts.key_type[i], 'clear_key': h2b(opts.key_data[i]), 'kcv': kcv})
|
||||||
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
|
p2 = opts.key_id
|
||||||
if len(opts.key_type) > 1:
|
if len(opts.key_type) > 1:
|
||||||
p2 |= 0x80
|
p2 |= 0x80
|
||||||
self.put_key(opts.old_key_version_nr, opts.key_version_nr, p2, kdb)
|
ADF_SD.put_key(self._cmd.lchan.scc, 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 = argparse.ArgumentParser()
|
||||||
get_status_parser.add_argument('subset', choices=list(StatusSubset.ksymapping.values()),
|
get_status_parser.add_argument('subset', choices=list(StatusSubset.ksymapping.values()),
|
||||||
@@ -661,30 +939,18 @@ class ADF_SD(CardADF):
|
|||||||
def do_get_status(self, opts):
|
def do_get_status(self, opts):
|
||||||
"""Perform GlobalPlatform GET STATUS command in order to retrieve status information
|
"""Perform GlobalPlatform GET STATUS command in order to retrieve status information
|
||||||
on Issuer Security Domain, Executable Load File, Executable Module or Applications."""
|
on Issuer Security Domain, Executable Load File, Executable Module or Applications."""
|
||||||
grd_list = self.get_status(opts.subset, opts.aid)
|
grd_list = ADF_SD.get_status(self._cmd.lchan.scc, opts.subset, opts.aid, self.gp_version())
|
||||||
for grd in grd_list:
|
for grd in grd_list:
|
||||||
self._cmd.poutput_json(grd.to_dict())
|
self._cmd.poutput_json(grd.to_dict())
|
||||||
|
|
||||||
def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpRegistryRelatedData]:
|
def gp_version(self) -> Optional[Tuple[int, ...]]:
|
||||||
subset_hex = b2h(build_construct(StatusSubset, subset))
|
"""GP version the selected SD reports in its Card Recognition
|
||||||
aid = ApplicationAID(decoded=aid_search_qualifier)
|
Data, e.g. (2, 1, 1). Card Recognition Data "shall be present" v2.1.1/v2.3.1 section 7.4.1.3,
|
||||||
cmd_data = aid.to_tlv() + h2b('5c054f9f70c5cc')
|
so this must succeed no matter the GP version. None if card did not answer GET DATA / OID unknown.
|
||||||
p2 = 0x02 # TLV format according to Table 11-36
|
Cached, it cannot change during a session."""
|
||||||
grd_list = []
|
if not hasattr(self, '_gp_version'):
|
||||||
while True:
|
self._gp_version = ADF_SD.gp_version(self._cmd.lchan.scc)
|
||||||
hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
|
return self._gp_version
|
||||||
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 = argparse.ArgumentParser()
|
||||||
set_status_parser.add_argument('scope', choices=list(SetStatusScope.ksymapping.values()),
|
set_status_parser.add_argument('scope', choices=list(SetStatusScope.ksymapping.values()),
|
||||||
@@ -699,14 +965,7 @@ class ADF_SD(CardADF):
|
|||||||
"""Perform GlobalPlatform SET STATUS command in order to change the life cycle state of the
|
"""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
|
Issuer Security Domain, Supplementary Security Domain or Application. This normally requires
|
||||||
prior authentication with a Secure Channel Protocol."""
|
prior authentication with a Secure Channel Protocol."""
|
||||||
self.set_status(opts.scope, opts.status, opts.aid)
|
ADF_SD.set_status(self._cmd.lchan.scc, 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 = argparse.ArgumentParser()
|
||||||
inst_perso_parser.add_argument('application_aid', type=is_hexstr, help='Application AID')
|
inst_perso_parser.add_argument('application_aid', type=is_hexstr, help='Application AID')
|
||||||
@@ -716,7 +975,8 @@ class ADF_SD(CardADF):
|
|||||||
"""Perform GlobalPlatform INSTALL [for personalization] command in order to inform a Security
|
"""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)."""
|
Domain that the following STORE DATA commands are meant for a specific AID (specified here)."""
|
||||||
# Section 11.5.2.3.6 / Table 11-47
|
# Section 11.5.2.3.6 / Table 11-47
|
||||||
self.install(0x20, 0x00, "0000%02x%s000000" % (len(opts.application_aid)//2, opts.application_aid))
|
ADF_SD.install(self._cmd.lchan.scc, 0x20, 0x00, "0000%02x%s000000" %
|
||||||
|
(len(opts.application_aid)//2, opts.application_aid))
|
||||||
|
|
||||||
inst_inst_parser = argparse.ArgumentParser()
|
inst_inst_parser = argparse.ArgumentParser()
|
||||||
inst_inst_parser.add_argument('--load-file-aid', type=is_hexstr, default='',
|
inst_inst_parser.add_argument('--load-file-aid', type=is_hexstr, default='',
|
||||||
@@ -751,7 +1011,7 @@ class ADF_SD(CardADF):
|
|||||||
# convert from list to "true-dict" as required by construct.FlagsEnum
|
# convert from list to "true-dict" as required by construct.FlagsEnum
|
||||||
decoded['privileges'] = {x: True for x in decoded['privileges']}
|
decoded['privileges'] = {x: True for x in decoded['privileges']}
|
||||||
ifi_bytes = build_construct(InstallForInstallCD, decoded)
|
ifi_bytes = build_construct(InstallForInstallCD, decoded)
|
||||||
self.install(p1, 0x00, b2h(ifi_bytes))
|
ADF_SD.install(self._cmd.lchan.scc, p1, 0x00, b2h(ifi_bytes))
|
||||||
|
|
||||||
inst_load_parser = argparse.ArgumentParser()
|
inst_load_parser = argparse.ArgumentParser()
|
||||||
inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True,
|
inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True,
|
||||||
@@ -776,11 +1036,7 @@ class ADF_SD(CardADF):
|
|||||||
'load_parameters'/Prefixed(Int8ub, GreedyBytes),
|
'load_parameters'/Prefixed(Int8ub, GreedyBytes),
|
||||||
'load_token'/Prefixed(Int8ub, GreedyBytes))
|
'load_token'/Prefixed(Int8ub, GreedyBytes))
|
||||||
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
|
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
|
||||||
self.install(0x02, 0x00, b2h(ifl_bytes))
|
ADF_SD.install(self._cmd.lchan.scc, 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 = argparse.ArgumentParser()
|
||||||
del_cc_parser.add_argument('aid', type=is_hexstr,
|
del_cc_parser.add_argument('aid', type=is_hexstr,
|
||||||
@@ -794,7 +1050,7 @@ class ADF_SD(CardADF):
|
|||||||
File, an Application or an Executable Load File and its related Applications."""
|
File, an Application or an Executable Load File and its related Applications."""
|
||||||
p2 = 0x80 if opts.delete_related_objects else 0x00
|
p2 = 0x80 if opts.delete_related_objects else 0x00
|
||||||
aid = ApplicationAID(decoded=opts.aid)
|
aid = ApplicationAID(decoded=opts.aid)
|
||||||
self.delete(0x00, p2, b2h(aid.to_tlv()))
|
ADF_SD.delete(self._cmd.lchan.scc, 0x00, p2, b2h(aid.to_tlv()))
|
||||||
|
|
||||||
del_key_parser = argparse.ArgumentParser()
|
del_key_parser = argparse.ArgumentParser()
|
||||||
del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)')
|
del_key_parser.add_argument('--key-id', type=auto_uint7, help='Key Identifier (KID)')
|
||||||
@@ -815,50 +1071,30 @@ class ADF_SD(CardADF):
|
|||||||
cmd += "d001%02x" % opts.key_id
|
cmd += "d001%02x" % opts.key_id
|
||||||
if opts.key_ver is not None:
|
if opts.key_ver is not None:
|
||||||
cmd += "d201%02x" % opts.key_ver
|
cmd += "d201%02x" % opts.key_ver
|
||||||
self.delete(0x00, p2, cmd)
|
ADF_SD.delete(self._cmd.lchan.scc, 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 = argparse.ArgumentParser()
|
||||||
load_parser_from_grp = load_parser.add_mutually_exclusive_group(required=True)
|
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-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-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_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)
|
@cmd2.with_argparser(load_parser)
|
||||||
def do_load(self, opts):
|
def do_load(self, opts):
|
||||||
"""Perform a GlobalPlatform LOAD command. (We currently only support loading without DAP and
|
"""Perform a GlobalPlatform LOAD command. (We currently only support loading without DAP and
|
||||||
without ciphering.)"""
|
without ciphering.)"""
|
||||||
if opts.from_hex is not None:
|
if opts.from_hex is not None:
|
||||||
self.load(h2b(opts.from_hex))
|
ADF_SD.load(self._cmd.lchan.scc, h2b(opts.from_hex), opts.chunk_len)
|
||||||
elif opts.from_file is not None:
|
elif opts.from_file is not None:
|
||||||
self.load(opts.from_file.read())
|
ADF_SD.load(self._cmd.lchan.scc, opts.from_file.read(), opts.chunk_len)
|
||||||
elif opts.from_cap_file is not None:
|
elif opts.from_cap_file is not None:
|
||||||
cap = CapFile(opts.from_cap_file)
|
cap = CapFile(opts.from_cap_file)
|
||||||
self.load(cap.get_loadfile())
|
ADF_SD.load(self._cmd.lchan.scc, cap.get_loadfile(), opts.chunk_len)
|
||||||
else:
|
else:
|
||||||
raise ValueError('load source not specified!')
|
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 = argparse.ArgumentParser(usage='%(prog)s FILE [--install-parameters | --install-parameters-*]')
|
||||||
install_cap_parser.add_argument('cap_file', type=str, metavar='FILE',
|
install_cap_parser.add_argument('cap_file', type=str, metavar='FILE',
|
||||||
help='JAVA-CARD CAP file to install')
|
help='JAVA-CARD CAP file to install')
|
||||||
@@ -881,6 +1117,8 @@ class ADF_SD(CardADF):
|
|||||||
install_cap_parser_inst_prm_grp.add_argument('--install-parameters-stk',
|
install_cap_parser_inst_prm_grp.add_argument('--install-parameters-stk',
|
||||||
type=is_hexstr, default=None,
|
type=is_hexstr, default=None,
|
||||||
help='Load Parameters (ETSI TS 102 226, section 8.2.1.3.2.1)')
|
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)
|
@cmd2.with_argparser(install_cap_parser)
|
||||||
def do_install_cap(self, opts):
|
def do_install_cap(self, opts):
|
||||||
@@ -919,7 +1157,7 @@ class ADF_SD(CardADF):
|
|||||||
self._cmd.poutput("step #1: install for load...")
|
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.do_install_for_load("--load-file-aid %s --security-domain-aid %s" % (load_file_aid, security_domain_aid))
|
||||||
self._cmd.poutput("step #2: load...")
|
self._cmd.poutput("step #2: load...")
|
||||||
self.load(load_file)
|
ADF_SD.load(self._cmd.lchan.scc, load_file, opts.chunk_len)
|
||||||
self._cmd.poutput("step #3: install_for_install (and make selectable)...")
|
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" %
|
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))
|
(load_file_aid, module_aid, application_aid, install_parameters))
|
||||||
@@ -959,7 +1197,7 @@ class ADF_SD(CardADF):
|
|||||||
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(8)
|
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))
|
kset = GpCardKeyset(opts.key_ver, h2b(opts.key_enc), h2b(opts.key_mac), h2b(opts.key_dek))
|
||||||
scp02 = SCP02(card_keys=kset)
|
scp02 = SCP02(card_keys=kset)
|
||||||
self._establish_scp(scp02, host_challenge, opts.security_level)
|
ADF_SD.establish_scp(self._cmd.lchan.scc, scp02, host_challenge, opts.security_level)
|
||||||
|
|
||||||
est_scp03_parser = deepcopy(est_scp02_parser)
|
est_scp03_parser = deepcopy(est_scp02_parser)
|
||||||
est_scp03_parser.description = None
|
est_scp03_parser.description = None
|
||||||
@@ -987,27 +1225,15 @@ class ADF_SD(CardADF):
|
|||||||
host_challenge = h2b(opts.host_challenge) if opts.host_challenge else get_random_bytes(s_mode)
|
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))
|
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)
|
scp03 = SCP03(card_keys=kset, s_mode = s_mode)
|
||||||
self._establish_scp(scp03, host_challenge, opts.security_level)
|
ADF_SD.establish_scp(self._cmd.lchan.scc, 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()
|
self._cmd.update_prompt()
|
||||||
|
|
||||||
|
|
||||||
def do_release_scp(self, _opts):
|
def do_release_scp(self, _opts):
|
||||||
"""Release a previously establiehed secure channel."""
|
"""Release a previously establiehed secure channel."""
|
||||||
if not self._cmd.lchan.scc.scp:
|
if not self._cmd.lchan.scc.scp:
|
||||||
self._cmd.poutput("Cannot release SCP as none is established")
|
self._cmd.poutput("Cannot release SCP as none is established")
|
||||||
return
|
return
|
||||||
self._cmd.lchan.scc.scp = None
|
ADF_SD.release_scp(self._cmd.lchan.scc)
|
||||||
self._cmd.update_prompt()
|
self._cmd.update_prompt()
|
||||||
|
|
||||||
|
|
||||||
@@ -1065,10 +1291,16 @@ def compute_kcv_aes(key:bytes) -> bytes:
|
|||||||
cipher = AES.new(key, AES.MODE_ECB)
|
cipher = AES.new(key, AES.MODE_ECB)
|
||||||
return cipher.encrypt(plaintext)
|
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
|
# dict is keyed by the string name of the KeyType enum above in this file
|
||||||
KCV_CALCULATOR = {
|
KCV_CALCULATOR = {
|
||||||
'aes': compute_kcv_aes,
|
'aes': compute_kcv_aes,
|
||||||
'des': compute_kcv_des,
|
'des': compute_kcv_des,
|
||||||
|
'tls_psk': compute_kcv_psk,
|
||||||
}
|
}
|
||||||
|
|
||||||
def compute_kcv(key_type: str, key: bytes) -> Optional[bytes]:
|
def compute_kcv(key_type: str, key: bytes) -> Optional[bytes]:
|
||||||
|
|||||||
@@ -182,6 +182,29 @@ class SCP(SecureChannel, abc.ABC):
|
|||||||
"""Should we perform R-ENC?"""
|
"""Should we perform R-ENC?"""
|
||||||
return self.security_level & 0x20
|
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:
|
def __str__(self) -> str:
|
||||||
return "%s[%02x]" % (self.__class__.__name__, self.security_level)
|
return "%s[%02x]" % (self.__class__.__name__, self.security_level)
|
||||||
|
|
||||||
@@ -215,11 +238,20 @@ class SCP(SecureChannel, abc.ABC):
|
|||||||
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
def gen_ext_auth_apdu(self, security_level: int = 0x01) -> bytes:
|
||||||
pass
|
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:
|
def encrypt_key(self, key: bytes) -> bytes:
|
||||||
"""Encrypt a key with the DEK."""
|
"""Encrypt a key with the DEK."""
|
||||||
num_pad = len(key) % self.sk.blocksize
|
if len(key) % self.sk.blocksize:
|
||||||
if num_pad:
|
# The kcv is right padded before encryption and the kcb
|
||||||
return bertlv_encode_len(len(key)) + self.dek_encrypt(key + b'\x00'*num_pad)
|
# 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))
|
||||||
return self.dek_encrypt(key)
|
return self.dek_encrypt(key)
|
||||||
|
|
||||||
def decrypt_key(self, encrypted_key:bytes) -> bytes:
|
def decrypt_key(self, encrypted_key:bytes) -> bytes:
|
||||||
@@ -232,9 +264,8 @@ class SCP(SecureChannel, abc.ABC):
|
|||||||
# Block provides the actual length of the key component value, which allows recovering the
|
# 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
|
# clear-text key component value after decryption of the encrypted key component value and removal
|
||||||
# of padding bytes.
|
# of padding bytes.
|
||||||
decrypted = self.dek_decrypt(encrypted_key)
|
key_len, remainder = bertlv_parse_len(encrypted_key)
|
||||||
key_len, remainder = bertlv_parse_len(decrypted)
|
return self.dek_decrypt(remainder)[:key_len]
|
||||||
return remainder[:key_len]
|
|
||||||
else:
|
else:
|
||||||
# If the length of the Key Component Block is a multiple of the block size of the encryption
|
# 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
|
# algorithm (i.e. 8 bytes for DES, 16 bytes for AES), then it shall be assumed that no padding
|
||||||
@@ -260,10 +291,8 @@ 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 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
|
# Key Version Number 0x01 is a non-spec special-case of sysmoUSIM-SJS1
|
||||||
kvn_ranges = [[0x01, 0x01], [0x20, 0x2f], [0x70, 0x70]]
|
kvn_ranges = [[0x01, 0x01], [0x20, 0x2f], [0x70, 0x70]]
|
||||||
|
# C-MAC (Single DES + final 3DES, B.1.2.2) is always one full DES block
|
||||||
def __init__(self, *args, **kwargs):
|
mac_len = 8
|
||||||
self.overhead = 8
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||||
# See also GPC section B.1.1.2, E.4.7, and E.4.1
|
# See also GPC section B.1.1.2, E.4.7, and E.4.1
|
||||||
@@ -338,10 +367,16 @@ class SCP02(SCP):
|
|||||||
# CMAC on modified APDU
|
# CMAC on modified APDU
|
||||||
mlc = lc + 8
|
mlc = lc + 8
|
||||||
clac = cla | CLA_SM
|
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)
|
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + data)
|
||||||
if self.do_cenc:
|
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)
|
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
|
||||||
data = k.encrypt(pad80(data, 8))
|
data = k.encrypt(padded_data)
|
||||||
lc = len(data)
|
lc = len(data)
|
||||||
|
|
||||||
lc += 8
|
lc += 8
|
||||||
@@ -477,9 +512,13 @@ class SCP03(SCP):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.s_mode = kwargs.pop('s_mode', 8)
|
self.s_mode = kwargs.pop('s_mode', 8)
|
||||||
self.overhead = self.s_mode
|
|
||||||
super().__init__(*args, **kwargs)
|
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:
|
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||||
cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
|
cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16)
|
||||||
return cipher.encrypt(plaintext)
|
return cipher.encrypt(plaintext)
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import enum
|
||||||
import cmd2
|
import cmd2
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ class PySimLogger:
|
|||||||
formatted_message = logging.Formatter.format(PySimLogger.__formatter, record)
|
formatted_message = logging.Formatter.format(PySimLogger.__formatter, record)
|
||||||
color = PySimLogger.colors.get(record.levelno)
|
color = PySimLogger.colors.get(record.levelno)
|
||||||
if color:
|
if color:
|
||||||
if isinstance(color, str):
|
if isinstance(color, str) and not isinstance(color, enum.Enum):
|
||||||
PySimLogger.print_callback(color + formatted_message + "\033[0m")
|
PySimLogger.print_callback(color + formatted_message + "\033[0m")
|
||||||
else:
|
else:
|
||||||
PySimLogger.print_callback(_style(formatted_message, fg = color))
|
PySimLogger.print_callback(_style(formatted_message, fg = color))
|
||||||
|
|||||||
+247
-10
@@ -18,10 +18,12 @@
|
|||||||
import zlib
|
import zlib
|
||||||
import abc
|
import abc
|
||||||
import struct
|
import struct
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple, List, Union
|
||||||
from construct import Enum, Int8ub, Int16ub, Struct, BitsInteger, BitStruct
|
from construct import ConstructError, Enum, Int8ub, Int16ub, Struct, BitsInteger, BitStruct
|
||||||
from construct import Flag, Padding, Switch, this, PrefixedArray, GreedyRange
|
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.construct import *
|
||||||
|
from osmocom.tlv import bertlv_encode_len
|
||||||
from osmocom.utils import b2h
|
from osmocom.utils import b2h
|
||||||
|
|
||||||
from pySim.sms import UserDataHeader
|
from pySim.sms import UserDataHeader
|
||||||
@@ -56,6 +58,217 @@ CompactRemoteResp = Struct('number_of_commands'/Int8ub,
|
|||||||
'last_status_word'/HexAdapter(Bytes(2)),
|
'last_status_word'/HexAdapter(Bytes(2)),
|
||||||
'last_response_data'/HexAdapter(GreedyBytes))
|
'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)
|
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)
|
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)
|
POR_REQ = Enum(BitsInteger(2), no_por=0, por_required=1, por_only_when_error=2)
|
||||||
@@ -149,13 +362,23 @@ class OtaDialect(abc.ABC):
|
|||||||
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
raise ValueError("Invalid rc_cc_ds: %s" % spi['rc_cc_ds'])
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
def encode_cmd(self, otak: OtaKeyset, tar: bytes, 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."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes) -> (object, Optional["CompactRemoteResp"]):
|
def decode_resp(self, otak: OtaKeyset, spi: dict, apdu: bytes,
|
||||||
"""Decode a response into a response packet and, if indicted (by a
|
remote_format: str = 'compact') -> (object, Optional[object]):
|
||||||
response status of `"por_ok"`) a decoded response.
|
"""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
|
||||||
|
|
||||||
The response packet's common characteristics are not fully determined,
|
The response packet's common characteristics are not fully determined,
|
||||||
and (so far) completely proprietary per dialect."""
|
and (so far) completely proprietary per dialect."""
|
||||||
@@ -335,7 +558,16 @@ class OtaDialectSms(OtaDialect):
|
|||||||
'secured_data'/GreedyBytes)
|
'secured_data'/GreedyBytes)
|
||||||
hdr_construct = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
|
hdr_construct = Struct('chl'/Int8ub, 'spi'/SPI, 'kic'/KIC, 'kid'/KID_CC, 'tar'/Bytes(3))
|
||||||
|
|
||||||
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict, apdu: bytes) -> bytes:
|
def encode_cmd(self, otak: OtaKeyset, tar: bytes, spi: dict,
|
||||||
|
apdu: 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)
|
||||||
|
|
||||||
# length of signature in octets
|
# length of signature in octets
|
||||||
len_sig = self._compute_sig_len(spi)
|
len_sig = self._compute_sig_len(spi)
|
||||||
pad_cnt = 0
|
pad_cnt = 0
|
||||||
@@ -446,7 +678,10 @@ class OtaDialectSms(OtaDialect):
|
|||||||
return hdr_dec['tar'], spi, apdu
|
return hdr_dec['tar'], spi, apdu
|
||||||
|
|
||||||
|
|
||||||
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes) -> ("OtaDialectSms.SmsResponsePacket", Optional["CompactRemoteResp"]):
|
def decode_resp(self, otak: OtaKeyset, spi: dict, data: bytes,
|
||||||
|
remote_format: str = 'compact') -> ("OtaDialectSms.SmsResponsePacket", Optional[object]):
|
||||||
|
if remote_format not in ('compact', 'expanded'):
|
||||||
|
raise ValueError("Invalid remote_format: %s ?!" % remote_format)
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = h2b(data)
|
data = h2b(data)
|
||||||
# plain-text POR: 027100000e0ab000110000000000000001612f
|
# plain-text POR: 027100000e0ab000110000000000000001612f
|
||||||
@@ -492,9 +727,11 @@ class OtaDialectSms(OtaDialect):
|
|||||||
else:
|
else:
|
||||||
raise OtaCheckError('Unknown por_rc_cc_ds: %s' % spi['por_rc_cc_ds'])
|
raise OtaCheckError('Unknown por_rc_cc_ds: %s' % spi['por_rc_cc_ds'])
|
||||||
|
|
||||||
# TODO: ExpandedRemoteResponse according to TS 102 226 5.2.2
|
|
||||||
if res.response_status == 'por_ok' and len(res['secured_data']):
|
if res.response_status == 'por_ok' and len(res['secured_data']):
|
||||||
dec = CompactRemoteResp.parse(res['secured_data'])
|
if remote_format == 'expanded':
|
||||||
|
dec = decode_expanded_resp(res['secured_data'])
|
||||||
|
else:
|
||||||
|
dec = CompactRemoteResp.parse(res['secured_data'])
|
||||||
else:
|
else:
|
||||||
dec = None
|
dec = None
|
||||||
return (res, dec)
|
return (res, dec)
|
||||||
|
|||||||
+109
-3
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import typing
|
import typing
|
||||||
import abc
|
import abc
|
||||||
|
import logging
|
||||||
from bidict import bidict
|
from bidict import bidict
|
||||||
from construct import Int8ub, Byte, Bit, Flag, BitsInteger
|
from construct import Int8ub, Byte, Bit, Flag, BitsInteger
|
||||||
from construct import Struct, Enum, Tell, BitStruct, this, Padding
|
from construct import Struct, Enum, Tell, BitStruct, this, Padding
|
||||||
@@ -28,6 +29,8 @@ from osmocom.utils import Hexstr, h2b, b2h
|
|||||||
|
|
||||||
from smpp.pdu import pdu_types, operations
|
from smpp.pdu import pdu_types, operations
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
BytesOrHex = typing.Union[Hexstr, bytes]
|
BytesOrHex = typing.Union[Hexstr, bytes]
|
||||||
|
|
||||||
class UserDataHeader:
|
class UserDataHeader:
|
||||||
@@ -60,6 +63,109 @@ class UserDataHeader:
|
|||||||
return self._construct.build({'ies':self.ies, 'data':b''})
|
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:
|
def smpp_dcs_is_8bit(dcs: pdu_types.DataCoding) -> bool:
|
||||||
"""Determine if the given SMPP data coding scheme is 8-bit or not."""
|
"""Determine if the given SMPP data coding scheme is 8-bit or not."""
|
||||||
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
||||||
@@ -140,8 +246,8 @@ class AddressField:
|
|||||||
def to_bytes(self) -> bytes:
|
def to_bytes(self) -> bytes:
|
||||||
"""Encode the AddressField into the binary representation as used in T-PDU."""
|
"""Encode the AddressField into the binary representation as used in T-PDU."""
|
||||||
num_digits = len(self.digits)
|
num_digits = len(self.digits)
|
||||||
if num_digits % 2:
|
# don't store the filler nibble or get_bytes() encodes it as digit and ends up too large
|
||||||
self.digits += 'f'
|
digits = self.digits + 'f' if num_digits % 2 else self.digits
|
||||||
d = {
|
d = {
|
||||||
'addr_len': num_digits,
|
'addr_len': num_digits,
|
||||||
'type_of_addr': {
|
'type_of_addr': {
|
||||||
@@ -149,7 +255,7 @@ class AddressField:
|
|||||||
'type_of_number': self.ton,
|
'type_of_number': self.ton,
|
||||||
'numbering_plan_id': self.npi,
|
'numbering_plan_id': self.npi,
|
||||||
},
|
},
|
||||||
'digits': self.digits,
|
'digits': digits,
|
||||||
}
|
}
|
||||||
return self._construct.build(d)
|
return self._construct.build(d)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 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,8 +45,10 @@ class ApduTracer:
|
|||||||
|
|
||||||
class StdoutApduTracer(ApduTracer):
|
class StdoutApduTracer(ApduTracer):
|
||||||
"""Minimalistic APDU tracer, printing commands to stdout."""
|
"""Minimalistic APDU tracer, printing commands to stdout."""
|
||||||
def trace_response(self, cmd, sw, resp):
|
def trace_command(self, cmd):
|
||||||
log.info("-> %s %s", cmd[:10], cmd[10:])
|
log.info("-> %s %s", cmd[:10], cmd[10:])
|
||||||
|
|
||||||
|
def trace_response(self, cmd, sw, resp):
|
||||||
log.info("<- %s: %s", sw, resp)
|
log.info("<- %s: %s", sw, resp)
|
||||||
|
|
||||||
def trace_reset(self):
|
def trace_reset(self):
|
||||||
@@ -70,10 +72,26 @@ class ProactiveHandler(abc.ABC):
|
|||||||
raise NotImplementedError('No handler method for %s' % pcmd.decoded)
|
raise NotImplementedError('No handler method for %s' % pcmd.decoded)
|
||||||
|
|
||||||
def prepare_response(self, pcmd: ProactiveCommand, general_result: str = 'performed_successfully'):
|
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.
|
# The Command Details are echoed from the command that has been processed.
|
||||||
(command_details,) = [c for c in pcmd.children if isinstance(c, CommandDetails)]
|
command_details = next((c for c in children if isinstance(c, CommandDetails)), None)
|
||||||
# invert the device identities
|
# invert the device identities
|
||||||
(command_dev_ids,) = [c for c in pcmd.children if isinstance(c, DeviceIdentities)]
|
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,))
|
||||||
rsp_dev_ids = DeviceIdentities()
|
rsp_dev_ids = DeviceIdentities()
|
||||||
rsp_dev_ids.from_dict({'device_identities': {
|
rsp_dev_ids.from_dict({'device_identities': {
|
||||||
'dest_dev_id': command_dev_ids.decoded['source_dev_id'],
|
'dest_dev_id': command_dev_ids.decoded['source_dev_id'],
|
||||||
@@ -317,6 +335,18 @@ class LinkBaseTpdu(LinkBase):
|
|||||||
# correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with
|
# 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
|
# 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 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:
|
while True:
|
||||||
if sw in ['9000', '9100']:
|
if sw in ['9000', '9100']:
|
||||||
# A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command)
|
# A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command)
|
||||||
@@ -329,7 +359,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
|
# 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)
|
# ISO/IEC 7816-4, Table 5)
|
||||||
le_gr = sw[2:4]
|
le_gr = sw[2:4]
|
||||||
elif sw[0:2] in ['62', '63']:
|
elif sw[0:2] in ['62', '63'] and dummy_gr_allowed:
|
||||||
# There are corner cases (status word is 62xx or 63xx) where the UICC/eUICC/SIM asks us
|
# 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.
|
# 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)
|
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4b and ETSI TS 151 011, section 9.4.1)
|
||||||
@@ -344,6 +374,7 @@ class LinkBaseTpdu(LinkBase):
|
|||||||
data_gr, sw = self.send_tpdu(tpdu_gr)
|
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)")
|
log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)")
|
||||||
data += data_gr
|
data += data_gr
|
||||||
|
dummy_gr_allowed = False
|
||||||
if sw[0:2] == '6c':
|
if sw[0:2] == '6c':
|
||||||
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
|
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
|
||||||
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
|
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
|
||||||
|
|||||||
+36
-11
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from bidict import bidict
|
from bidict import bidict
|
||||||
|
import copy
|
||||||
|
|
||||||
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
|
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
|
||||||
from construct import Optional as COptional, Computed
|
from construct import Optional as COptional, Computed
|
||||||
@@ -335,6 +336,8 @@ 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
|
# ETSI TS 102 221 Section 9.2.7 + ISO7816-4 9.3.3/9.3.4
|
||||||
class _AM_DO_DF(DataObject):
|
class _AM_DO_DF(DataObject):
|
||||||
|
"""ISO7816-4:2005 5.4.3.1 Table 16"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('access_mode', 'Access Mode', tag=0x80)
|
super().__init__('access_mode', 'Access Mode', tag=0x80)
|
||||||
|
|
||||||
@@ -381,7 +384,7 @@ class _AM_DO_DF(DataObject):
|
|||||||
|
|
||||||
|
|
||||||
class _AM_DO_EF(DataObject):
|
class _AM_DO_EF(DataObject):
|
||||||
"""ISO7816-4 9.3.2 Table 18 + 9.3.3.1 Table 31"""
|
"""ISO7816-4:2005 5.4.3.1 Table 17"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('access_mode', 'Access Mode', tag=0x80)
|
super().__init__('access_mode', 'Access Mode', tag=0x80)
|
||||||
@@ -429,7 +432,7 @@ class _AM_DO_EF(DataObject):
|
|||||||
|
|
||||||
|
|
||||||
class _AM_DO_CHDR(DataObject):
|
class _AM_DO_CHDR(DataObject):
|
||||||
"""Command Header Access Mode DO according to ISO 7816-4 Table 32."""
|
"""Command Header Access Mode DO according to ISO 7816-4:2005 5.4.3.2 Table 22."""
|
||||||
|
|
||||||
def __init__(self, tag):
|
def __init__(self, tag):
|
||||||
super().__init__('command_header', 'Command Header Description', tag=tag)
|
super().__init__('command_header', 'Command Header Description', tag=tag)
|
||||||
@@ -543,8 +546,9 @@ class CRT_DO(DataObject):
|
|||||||
pin = pin_names.inverse[self.decoded]
|
pin = pin_names.inverse[self.decoded]
|
||||||
return b'\x83\x01' + pin.to_bytes(1, 'big') + b'\x95\x01\x08'
|
return b'\x83\x01' + pin.to_bytes(1, 'big') + b'\x95\x01\x08'
|
||||||
|
|
||||||
# ISO7816-4 9.3.3 Table 33
|
|
||||||
class SecCondByte_DO(DataObject):
|
class SecCondByte_DO(DataObject):
|
||||||
|
"""ISO7816-4:2005 5.4.3.1 Table 20"""
|
||||||
|
|
||||||
def __init__(self, tag=0x9d):
|
def __init__(self, tag=0x9d):
|
||||||
super().__init__('security_condition_byte', tag=tag)
|
super().__init__('security_condition_byte', tag=tag)
|
||||||
|
|
||||||
@@ -732,36 +736,57 @@ class EF_ARR(LinFixedEF):
|
|||||||
raise ValueError
|
raise ValueError
|
||||||
return by_mode
|
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):
|
def _decode_record_bin(self, raw_bin_data, **kwargs):
|
||||||
# we can only guess if we should decode for EF or DF here :(
|
# we can only guess if we should decode for EF or DF here, but our caller may
|
||||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
# be able to pass us a hint:
|
||||||
|
arr_seq = self.__get_do_sequence(kwargs.get('decode_for_df', False))
|
||||||
dec = arr_seq.decode_multi(raw_bin_data)
|
dec = arr_seq.decode_multi(raw_bin_data)
|
||||||
# we cannot pass the result through flatten() here, as we don't have a related
|
# 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 :(
|
# 'un-flattening' decoder, and hence would be unable to encode :(
|
||||||
return dec[0]
|
return dec[0]
|
||||||
|
|
||||||
def _encode_record_bin(self, in_json, **kwargs):
|
def _encode_record_bin(self, in_json, **kwargs):
|
||||||
# we can only guess if we should decode for EF or DF here :(
|
# we can only guess if we should decode for EF or DF here, but our caller may
|
||||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
# be able to pass us a hint:
|
||||||
|
arr_seq = self.__get_do_sequence(kwargs.get('encode_for_df', False))
|
||||||
return arr_seq.encode_multi(in_json)
|
return arr_seq.encode_multi(in_json)
|
||||||
|
|
||||||
@with_default_category('File-Specific Commands')
|
@with_default_category('File-Specific Commands')
|
||||||
class AddlShellCommands(CommandSet):
|
class AddlShellCommands(CommandSet):
|
||||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
|
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)
|
||||||
def do_read_arr_record(self, opts):
|
def do_read_arr_record(self, opts):
|
||||||
"""Read one EF.ARR record in flattened, human-friendly form."""
|
"""Read one EF.ARR record in flattened, human-friendly form."""
|
||||||
(data, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
|
(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 = self._cmd.lchan.selected_file.flatten(data)
|
data = self._cmd.lchan.selected_file.flatten(data)
|
||||||
self._cmd.poutput_json(data, opts.oneline)
|
self._cmd.poutput_json(data, opts.oneline)
|
||||||
|
|
||||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
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)
|
||||||
def do_read_arr_records(self, opts):
|
def do_read_arr_records(self, opts):
|
||||||
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
|
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
|
||||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
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
|
# collect all results in list so they are rendered as JSON list when printing
|
||||||
data_list = []
|
data_list = []
|
||||||
for recnr in range(1, 1 + num_of_rec):
|
for recnr in range(1, 1 + num_of_rec):
|
||||||
(data, _sw) = self._cmd.lchan.read_record_dec(recnr)
|
(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 = self._cmd.lchan.selected_file.flatten(data)
|
data = self._cmd.lchan.selected_file.flatten(data)
|
||||||
data_list.append(data)
|
data_list.append(data)
|
||||||
self._cmd.poutput_json(data_list, opts.oneline)
|
self._cmd.poutput_json(data_list, opts.oneline)
|
||||||
|
|||||||
@@ -0,0 +1,417 @@
|
|||||||
|
#!/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())
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#!/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,6 +27,7 @@ import pySim.ts_31_102
|
|||||||
import pySim.ts_31_103
|
import pySim.ts_31_103
|
||||||
import pySim.ts_51_011
|
import pySim.ts_51_011
|
||||||
import pySim.sysmocom_sja2
|
import pySim.sysmocom_sja2
|
||||||
|
import pySim.sysmocom_sjs1
|
||||||
import pySim.gsm_r
|
import pySim.gsm_r
|
||||||
import pySim.cdma_ruim
|
import pySim.cdma_ruim
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import logging
|
import logging
|
||||||
|
import hashlib
|
||||||
|
from types import SimpleNamespace
|
||||||
from osmocom.utils import b2h, h2b
|
from osmocom.utils import b2h, h2b
|
||||||
|
from osmocom.tlv import bertlv_encode_len
|
||||||
|
|
||||||
from pySim.global_platform import *
|
from pySim.global_platform import *
|
||||||
from pySim.global_platform.scp import *
|
from pySim.global_platform.scp import *
|
||||||
@@ -283,6 +286,41 @@ class SCP03_Test_AES256_33(SCP03_Test, unittest.TestCase):
|
|||||||
# FIXME: test auth with random (0x60) vs pseudo-random (0x70) challenge
|
# 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):
|
class SCP03_KCV_Test(unittest.TestCase):
|
||||||
def test_kcv(self):
|
def test_kcv(self):
|
||||||
self.assertEqual(compute_kcv('aes', KEYSET_AES128.enc), h2b('C35280'))
|
self.assertEqual(compute_kcv('aes', KEYSET_AES128.enc), h2b('C35280'))
|
||||||
@@ -290,6 +328,204 @@ class SCP03_KCV_Test(unittest.TestCase):
|
|||||||
self.assertEqual(compute_kcv('aes', KEYSET_AES128.dek), h2b('840DE5'))
|
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):
|
class Install_param_Test(unittest.TestCase):
|
||||||
def test_gen_install_parameters(self):
|
def test_gen_install_parameters(self):
|
||||||
load_parameters = gen_install_parameters(256, 256, '010001001505000000000000000000000000')
|
load_parameters = gen_install_parameters(256, 256, '010001001505000000000000000000000000')
|
||||||
@@ -298,5 +534,326 @@ class Install_param_Test(unittest.TestCase):
|
|||||||
load_parameters = gen_install_parameters()
|
load_parameters = gen_install_parameters()
|
||||||
self.assertEqual(load_parameters, 'c900')
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -20,11 +20,20 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import logging
|
import logging
|
||||||
|
import cmd2
|
||||||
|
from packaging import version
|
||||||
from pySim.log import PySimLogger
|
from pySim.log import PySimLogger
|
||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
from inspect import currentframe, getframeinfo
|
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__)
|
log = PySimLogger.get(__name__)
|
||||||
|
|
||||||
TEST_MSG_DEBUG = "this is a debug message"
|
TEST_MSG_DEBUG = "this is a debug message"
|
||||||
@@ -37,6 +46,17 @@ expected_message = None
|
|||||||
|
|
||||||
class PySimLogger_Test(unittest.TestCase):
|
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):
|
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
|
# 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.
|
# logged to stdout, just as if it were printed via a normal print() statement.
|
||||||
@@ -117,5 +137,18 @@ class PySimLogger_Test(unittest.TestCase):
|
|||||||
expected_message = "CRITICAL: " + TEST_MSG_CRITICAL
|
expected_message = "CRITICAL: " + TEST_MSG_CRITICAL
|
||||||
log.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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -300,5 +300,292 @@ class SmsOtaTestCase(OtaTestCase):
|
|||||||
self.assertEqual(d.last_status_word, t['response']['last_status_word'])
|
self.assertEqual(d.last_status_word, t['response']['last_status_word'])
|
||||||
self.assertEqual(d.last_response_data, t['response']['last_response_data'])
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
#!/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,3 +103,126 @@ class Test_DELIVER(unittest.TestCase):
|
|||||||
self.assertEqual(d.tp_pid, 0x7f)
|
self.assertEqual(d.tp_pid, 0x7f)
|
||||||
self.assertEqual(d.tp_dcs, 0xf6)
|
self.assertEqual(d.tp_dcs, 0xf6)
|
||||||
self.assertEqual(d.tp_udl, 8)
|
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,6 +21,7 @@ import logging
|
|||||||
from osmocom.utils import b2h, h2b, all_subclasses
|
from osmocom.utils import b2h, h2b, all_subclasses
|
||||||
from osmocom.tlv import *
|
from osmocom.tlv import *
|
||||||
|
|
||||||
|
import pySim.cat
|
||||||
import pySim.iso7816_4
|
import pySim.iso7816_4
|
||||||
import pySim.ts_102_221
|
import pySim.ts_102_221
|
||||||
import pySim.ts_102_222
|
import pySim.ts_102_222
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
#!/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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn21AesEnc(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn22AesMac(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn30AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn31AesEnc(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp03Kvn32AesMac(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp80Kvn01DesDek(val= '01020304050607080910111213141516':str)
|
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
|
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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-20-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-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)
|
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
|
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-20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN20-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-21-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn21AesEnc(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-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)
|
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
|
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-21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN21-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP02-22-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn22AesMac(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-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)
|
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
|
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-22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP02-KVN22-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-30-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-AES-DEK': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn30AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-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)
|
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
|
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-30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN30-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-AES-ENC': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-31-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-AES-ENC': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-AES-ENC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn31AesEnc(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-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)
|
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
|
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-31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN31-AES-ENC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= '01020304050607080910111213141516':str)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-AES-MAC': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= 11020304050607080910111213141516:int)
|
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
|
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
|
||||||
read_back_val= {'SCP03-32-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-AES-MAC': '11020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= '010203040506070809101112131415161718192021222324':str)
|
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
|
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-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-AES-MAC': '010203040506070809101112131415161718192021222324'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp03Kvn32AesMac(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
|
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
|
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-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-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)
|
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
|
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-32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
read_back_val= {'SCP03-KVN32-AES-MAC': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp80Kvn01DesDek(val= '01020304050607080910111213141516':str)
|
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
|
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