From efe6e321201b539e8c22e554c770ba0f1d846114 Mon Sep 17 00:00:00 2001 From: Philipp Maier Date: Thu, 9 Apr 2026 14:09:52 +0200 Subject: [PATCH] WIP: Remote Card Procedure Framework Problem: When UICC/eUICC cards are deployed into the field it is often difficult to perform modifications to those cards. One important factor that makes after-deployment modifications often difficult is that the key material needed to perform the task must not be handed to the card holder due to security requirements. The presented Remote Card Procedure Framework solves this problem. It provides a so called Remote Card Procedure Client (RCPC), which is a lightwight software client which can be run by the card holder on the remote machine. With the RCPC, the card holder can access a so called Remote Card Procedure Server (RCPC), to which so called Remote Card Procedure Modules (RCPM) can subscribe and publish their functionality. With the RCPC, the card holder can browse the functionality offered by those connected modules and eventually the card holder may execute a certain procedure by passing a command to the RCPS. When a procedure is carried out, the RCPS automatically retrieves the required key material from a database or CSV file and passes those keys on to the selected RCPM. The RCPM can then use the key material to establish a secure channel to carry out the procedure. The procedure is then protected by a secure channel and the key material is never disclosed towards the card holder on the remote end. The framework is desinged in such a way that existing pySim APIs and functions can be used from the RCPM API user code. Also only minimal boilerplate code is required. The implementation also ships with a comprehensive example. Related: SYS#6959 --- contrib/rcp/rcp_client.py | 243 ++++++ contrib/rcp/rcp_module_utils.py | 424 +++++++++++ contrib/rcp/rcp_server.py | 666 +++++++++++++++++ contrib/rcp/rcp_utils.py | 330 +++++++++ contrib/rcp/rcpc_to_rcps_schema.json | 99 +++ contrib/rcp/rcpm_to_rcps_schema.json | 127 ++++ contrib/rcp/rcpmcs_to_rcps_schema.json | 36 + contrib/rcp/rcps_to_rcpc_schema.json | 125 ++++ contrib/rcp/rcps_to_rcpm_schema.json | 25 + contrib/rcp/rcps_to_rcpmcs_schema.json | 106 +++ contrib/rcp/usage_example/card_data.csv | 2 + contrib/rcp/usage_example/card_data.csv.encr | 2 + .../certs/example_ssl_rcp_ca_cert.crt | 20 + .../certs/example_ssl_rcpc_rcps_cert.pem | 115 +++ .../certs/example_ssl_rcpm_rcps_cert.pem | 115 +++ .../certs/example_ssl_rcps_rcpm_cert.pem | 115 +++ contrib/rcp/usage_example/certs/make_certs.sh | 49 ++ .../rcp/usage_example/encrypt_card_data.sh | 8 + contrib/rcp/usage_example/params.cfg | 36 + contrib/rcp/usage_example/rcp_module.py | 130 ++++ contrib/rcp/usage_example/readme.txt | 16 + contrib/rcp/usage_example/run_rcp_client.sh | 29 + .../rcp/usage_example/run_rcp_client_cmd.sh | 28 + .../rcp/usage_example/run_rcp_client_help.sh | 6 + .../usage_example/run_rcp_client_help_cmd.sh | 9 + .../run_rcp_client_help_cmd_specific.sh | 28 + contrib/rcp/usage_example/start_rcp_module.sh | 14 + contrib/rcp/usage_example/start_rcp_server.sh | 13 + docs/conf.py | 3 + docs/index.rst | 1 + docs/rcpf.rst | 690 ++++++++++++++++++ 31 files changed, 3610 insertions(+) create mode 100755 contrib/rcp/rcp_client.py create mode 100644 contrib/rcp/rcp_module_utils.py create mode 100755 contrib/rcp/rcp_server.py create mode 100644 contrib/rcp/rcp_utils.py create mode 100644 contrib/rcp/rcpc_to_rcps_schema.json create mode 100644 contrib/rcp/rcpm_to_rcps_schema.json create mode 100644 contrib/rcp/rcpmcs_to_rcps_schema.json create mode 100644 contrib/rcp/rcps_to_rcpc_schema.json create mode 100644 contrib/rcp/rcps_to_rcpm_schema.json create mode 100644 contrib/rcp/rcps_to_rcpmcs_schema.json create mode 100644 contrib/rcp/usage_example/card_data.csv create mode 100644 contrib/rcp/usage_example/card_data.csv.encr create mode 100644 contrib/rcp/usage_example/certs/example_ssl_rcp_ca_cert.crt create mode 100644 contrib/rcp/usage_example/certs/example_ssl_rcpc_rcps_cert.pem create mode 100644 contrib/rcp/usage_example/certs/example_ssl_rcpm_rcps_cert.pem create mode 100644 contrib/rcp/usage_example/certs/example_ssl_rcps_rcpm_cert.pem create mode 100755 contrib/rcp/usage_example/certs/make_certs.sh create mode 100755 contrib/rcp/usage_example/encrypt_card_data.sh create mode 100644 contrib/rcp/usage_example/params.cfg create mode 100755 contrib/rcp/usage_example/rcp_module.py create mode 100644 contrib/rcp/usage_example/readme.txt create mode 100755 contrib/rcp/usage_example/run_rcp_client.sh create mode 100755 contrib/rcp/usage_example/run_rcp_client_cmd.sh create mode 100755 contrib/rcp/usage_example/run_rcp_client_help.sh create mode 100755 contrib/rcp/usage_example/run_rcp_client_help_cmd.sh create mode 100755 contrib/rcp/usage_example/run_rcp_client_help_cmd_specific.sh create mode 100755 contrib/rcp/usage_example/start_rcp_module.sh create mode 100755 contrib/rcp/usage_example/start_rcp_server.sh create mode 100644 docs/rcpf.rst diff --git a/contrib/rcp/rcp_client.py b/contrib/rcp/rcp_client.py new file mode 100755 index 00000000..2cb815ee --- /dev/null +++ b/contrib/rcp/rcp_client.py @@ -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 . + +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) + + diff --git a/contrib/rcp/rcp_module_utils.py b/contrib/rcp/rcp_module_utils.py new file mode 100644 index 00000000..5cd7b1bb --- /dev/null +++ b/contrib/rcp/rcp_module_utils.py @@ -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 . + + +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) diff --git a/contrib/rcp/rcp_server.py b/contrib/rcp/rcp_server.py new file mode 100755 index 00000000..2e959c97 --- /dev/null +++ b/contrib/rcp/rcp_server.py @@ -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 . + +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) + diff --git a/contrib/rcp/rcp_utils.py b/contrib/rcp/rcp_utils.py new file mode 100644 index 00000000..128c995b --- /dev/null +++ b/contrib/rcp/rcp_utils.py @@ -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 . + +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 occured.""" + 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 + inculde 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") diff --git a/contrib/rcp/rcpc_to_rcps_schema.json b/contrib/rcp/rcpc_to_rcps_schema.json new file mode 100644 index 00000000..52bc5f41 --- /dev/null +++ b/contrib/rcp/rcpc_to_rcps_schema.json @@ -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-9,A-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-9,A-F]{0,512}$" + }, + "sw": { + "type": "string", + "pattern": "^[0-9,A-F]{0,4}$" + } + }, + "required": [ "data", "sw" ], + "additionalProperties": false + }, + "atr": { + "type": "string", + "pattern": "^[0-9,A-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 +} diff --git a/contrib/rcp/rcpm_to_rcps_schema.json b/contrib/rcp/rcpm_to_rcps_schema.json new file mode 100644 index 00000000..ab4d704a --- /dev/null +++ b/contrib/rcp/rcpm_to_rcps_schema.json @@ -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-9,A-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 +} diff --git a/contrib/rcp/rcpmcs_to_rcps_schema.json b/contrib/rcp/rcpmcs_to_rcps_schema.json new file mode 100644 index 00000000..81a46d56 --- /dev/null +++ b/contrib/rcp/rcpmcs_to_rcps_schema.json @@ -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-9,A-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 +} diff --git a/contrib/rcp/rcps_to_rcpc_schema.json b/contrib/rcp/rcps_to_rcpc_schema.json new file mode 100644 index 00000000..f88924f4 --- /dev/null +++ b/contrib/rcp/rcps_to_rcpc_schema.json @@ -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-9,A-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 +} diff --git a/contrib/rcp/rcps_to_rcpm_schema.json b/contrib/rcp/rcps_to_rcpm_schema.json new file mode 100644 index 00000000..5e16fb3d --- /dev/null +++ b/contrib/rcp/rcps_to_rcpm_schema.json @@ -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 +} diff --git a/contrib/rcp/rcps_to_rcpmcs_schema.json b/contrib/rcp/rcps_to_rcpmcs_schema.json new file mode 100644 index 00000000..e3615242 --- /dev/null +++ b/contrib/rcp/rcps_to_rcpmcs_schema.json @@ -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-9,A-F]{0,512}$" + }, + "sw": { + "type": "string", + "pattern": "^[0-9,A-F]{0,4}$" + } + }, + "required": [ "data", "sw" ], + "additionalProperties": false + }, + "atr": { + "type": "string", + "pattern": "^[0-9,A-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 +} diff --git a/contrib/rcp/usage_example/card_data.csv b/contrib/rcp/usage_example/card_data.csv new file mode 100644 index 00000000..132b7877 --- /dev/null +++ b/contrib/rcp/usage_example/card_data.csv @@ -0,0 +1,2 @@ +iccid,kic,kid,kik +8949440000001155306,F09C43EE1A0391665CC9F05AF4E0BD10,01981F4A20999F62AF99988007BAF6CA,8F8AEE5CDCC5D361368BC45673D99195 diff --git a/contrib/rcp/usage_example/card_data.csv.encr b/contrib/rcp/usage_example/card_data.csv.encr new file mode 100644 index 00000000..fc55dcce --- /dev/null +++ b/contrib/rcp/usage_example/card_data.csv.encr @@ -0,0 +1,2 @@ +"ICCID","KIC","KID","KIK" +"8949440000001155306","eae46224fa0a4ac1c12cba9d102f1188","3f14b978ddb38c08d832d4e4c2e0639d","9e19db4a5ed5cb8c4f5d96283eab273a" diff --git a/contrib/rcp/usage_example/certs/example_ssl_rcp_ca_cert.crt b/contrib/rcp/usage_example/certs/example_ssl_rcp_ca_cert.crt new file mode 100644 index 00000000..ab656d1f --- /dev/null +++ b/contrib/rcp/usage_example/certs/example_ssl_rcp_ca_cert.crt @@ -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----- diff --git a/contrib/rcp/usage_example/certs/example_ssl_rcpc_rcps_cert.pem b/contrib/rcp/usage_example/certs/example_ssl_rcpc_rcps_cert.pem new file mode 100644 index 00000000..2c401392 --- /dev/null +++ b/contrib/rcp/usage_example/certs/example_ssl_rcpc_rcps_cert.pem @@ -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----- diff --git a/contrib/rcp/usage_example/certs/example_ssl_rcpm_rcps_cert.pem b/contrib/rcp/usage_example/certs/example_ssl_rcpm_rcps_cert.pem new file mode 100644 index 00000000..429c09ad --- /dev/null +++ b/contrib/rcp/usage_example/certs/example_ssl_rcpm_rcps_cert.pem @@ -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----- diff --git a/contrib/rcp/usage_example/certs/example_ssl_rcps_rcpm_cert.pem b/contrib/rcp/usage_example/certs/example_ssl_rcps_rcpm_cert.pem new file mode 100644 index 00000000..5adc3c17 --- /dev/null +++ b/contrib/rcp/usage_example/certs/example_ssl_rcps_rcpm_cert.pem @@ -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----- diff --git a/contrib/rcp/usage_example/certs/make_certs.sh b/contrib/rcp/usage_example/certs/make_certs.sh new file mode 100755 index 00000000..a3d91644 --- /dev/null +++ b/contrib/rcp/usage_example/certs/make_certs.sh @@ -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 diff --git a/contrib/rcp/usage_example/encrypt_card_data.sh b/contrib/rcp/usage_example/encrypt_card_data.sh new file mode 100755 index 00000000..7f7b635b --- /dev/null +++ b/contrib/rcp/usage_example/encrypt_card_data.sh @@ -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 diff --git a/contrib/rcp/usage_example/params.cfg b/contrib/rcp/usage_example/params.cfg new file mode 100644 index 00000000..0463a3d6 --- /dev/null +++ b/contrib/rcp/usage_example/params.cfg @@ -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" diff --git a/contrib/rcp/usage_example/rcp_module.py b/contrib/rcp/usage_example/rcp_module.py new file mode 100755 index 00000000..6bc36dad --- /dev/null +++ b/contrib/rcp/usage_example/rcp_module.py @@ -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 . + +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") diff --git a/contrib/rcp/usage_example/readme.txt b/contrib/rcp/usage_example/readme.txt new file mode 100644 index 00000000..4bef46df --- /dev/null +++ b/contrib/rcp/usage_example/readme.txt @@ -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) diff --git a/contrib/rcp/usage_example/run_rcp_client.sh b/contrib/rcp/usage_example/run_rcp_client.sh new file mode 100755 index 00000000..4d32be24 --- /dev/null +++ b/contrib/rcp/usage_example/run_rcp_client.sh @@ -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 "" diff --git a/contrib/rcp/usage_example/run_rcp_client_cmd.sh b/contrib/rcp/usage_example/run_rcp_client_cmd.sh new file mode 100755 index 00000000..747dfdcc --- /dev/null +++ b/contrib/rcp/usage_example/run_rcp_client_cmd.sh @@ -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 diff --git a/contrib/rcp/usage_example/run_rcp_client_help.sh b/contrib/rcp/usage_example/run_rcp_client_help.sh new file mode 100755 index 00000000..69877296 --- /dev/null +++ b/contrib/rcp/usage_example/run_rcp_client_help.sh @@ -0,0 +1,6 @@ +#!/bin/bash +. ./params.cfg + +set -x +PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \ + -h diff --git a/contrib/rcp/usage_example/run_rcp_client_help_cmd.sh b/contrib/rcp/usage_example/run_rcp_client_help_cmd.sh new file mode 100755 index 00000000..20bde597 --- /dev/null +++ b/contrib/rcp/usage_example/run_rcp_client_help_cmd.sh @@ -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 diff --git a/contrib/rcp/usage_example/run_rcp_client_help_cmd_specific.sh b/contrib/rcp/usage_example/run_rcp_client_help_cmd_specific.sh new file mode 100755 index 00000000..a7198388 --- /dev/null +++ b/contrib/rcp/usage_example/run_rcp_client_help_cmd_specific.sh @@ -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 diff --git a/contrib/rcp/usage_example/start_rcp_module.sh b/contrib/rcp/usage_example/start_rcp_module.sh new file mode 100755 index 00000000..7ac7f80d --- /dev/null +++ b/contrib/rcp/usage_example/start_rcp_module.sh @@ -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 + diff --git a/contrib/rcp/usage_example/start_rcp_server.sh b/contrib/rcp/usage_example/start_rcp_server.sh new file mode 100755 index 00000000..a7f61148 --- /dev/null +++ b/contrib/rcp/usage_example/start_rcp_server.sh @@ -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 diff --git a/docs/conf.py b/docs/conf.py index 2e23faea..ec4407fa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,6 +14,8 @@ import os import sys sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('.')) # for local extensions (pysim_fs_sphinx, ...) +sys.path.insert(0, os.path.abspath('../contrib/rcp')) # for argparse +sys.path.insert(0, os.path.abspath('../contrib/rcp/usage_example')) # for argparse # -- Project information ----------------------------------------------------- @@ -42,6 +44,7 @@ extensions = [ "sphinx.ext.autosectionlabel", "sphinx.ext.napoleon", "pysim_fs_sphinx", + "sphinx.ext.graphviz", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/index.rst b/docs/index.rst index 8908c4ed..2396aa9d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -50,6 +50,7 @@ pySim consists of several parts: suci-keytool saip-tool smpp-ota-tool + rcpf Indices and tables diff --git a/docs/rcpf.rst b/docs/rcpf.rst new file mode 100644 index 00000000..fb21a11c --- /dev/null +++ b/docs/rcpf.rst @@ -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 a list and connections to custom modules `(RCP Modules)`, where +each module implements a set of procedures (commands). Based on its 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 users can 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 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` support 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 in 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` and its API will find the implementation of custom `RCP Modules` +as simple as implementing a new `pySim-shell` 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` 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 right +away 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 shello-script `run_rcp_client.sh` that which +requests commandline help and requests procedures by calling other scripts. +However to get an understanding 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 derived class. 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 +`RcpModule`. + +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.