diff --git a/contrib/rcp/rcp_client.py b/contrib/rcp/rcp_client.py
new file mode 100755
index 00000000..905da60e
--- /dev/null
+++ b/contrib/rcp/rcp_client.py
@@ -0,0 +1,204 @@
+#!/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 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
+from pySim.transport import init_reader, argparse_add_reader_args, LinkBase
+
+SERVER_TIMEOUT = 10
+
+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 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:
+ client = RcpcCltConnHdlr(sl, websocket, SERVER_TIMEOUT)
+
+ # Retrieve module description
+ module_descrs = await client.describe({"atr" : card_atr})
+
+ # Complete the commandlie 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 helpscreen 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 helpscreen.
+ 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..edc97ebc
--- /dev/null
+++ b/contrib/rcp/rcp_module_utils.py
@@ -0,0 +1,387 @@
+#!/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
+from websockets.sync.server import serve, ServerConnection
+
+# 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 = {'rcpc_instr': {'c_apdu' : apdu.upper()}}
+ rx_json = self.conn_hdlr._transact(tx_json)
+ data = rx_json['rcpc_result']['r_apdu']['data']
+ sw = rx_json['rcpc_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 enitity 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 = {'rcpc_instr': {'reset' : None}}
+ rx_json = self.conn_hdlr._transact(tx_json)
+ self._atr = rx_json['rcpc_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 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,
+ 'retrieve_keys' : {
+ 'euicc' : self.module.retrieve_euicc_keys,
+ 'uicc' : self.module.retrieve_uicc_keys
+ },
+ '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 RCPM 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 specificaton dict is directly
+ # passed to agparse on the client side.
+ #
+ # Example:
+ # [{"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"},
+ # }
+ # ]}
+ # ]
+ cmd_descr = []
+
+ # List with UICC (or eSIM) keys (columns) that the RCP Server shall retrieve before a command is executed.
+ # Execution will not continue in case any of the requested keys is not found.
+ # (see also: pySim.card_key_provider)
+ #
+ # Example: ['kic1', 'kid1', 'kik1']
+ retrieve_uicc_keys = []
+
+ # Same as retrieve_uicc_keys (see above), but only applicable with eUICCs
+ #
+ # Example: ['isdr_kic1', 'isdr_kid1', 'isdr_kik1']
+ retrieve_euicc_keys = []
+
+ # 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 above).
+ #
+ # Example:
+ # def cmd_reset(self, hdlr: RcpModuleHdlr) -> int: ...
+ # def cmd_read_binary(self, hdlr: RcpModuleHdlr) -> int: ...
+
+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, *args, **kwargs):
+ SrvSyncConnHdlr.__init__(self, *args, *kwargs)
+ self.module = module
+
+ 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 = {'rcpc_instr': {'print' : message}}
+ rx_json = self._transact(tx_json)
+ if rx_json != {'rcpc_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['rcpc_command']['cmd']
+ cmd_argv = rx_json['rcpc_command']['cmd_argv']
+ keys = rx_json['rcpc_command']['keys']
+ keys_uicc = dict_from_key_value_pairs(keys['uicc'], keylabel='key', valuelabel='value')
+ keys_euicc = dict_from_key_value_pairs(keys['euicc'], keylabel='key', valuelabel='value')
+
+ log.info(str(self) + " -- executing command: %s %s", cmd, " ".join(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)
+
+ # TODO: Perform a proper setup, similar to the one we have in pySim-shell, so that we have proper
+ # runtime states and full access to the pySim API
+ self.scc = SimCardCommands(transport=RcpsSimLink(self))
+ self.scc.cla_byte = "00"
+ self.scc.sel_ctrl = "0004"
+
+ # Hand over control to the command method provided by the specific module implementation
+ try:
+ rcp_module_hdlr = RcpModuleHdlr(self, cmd_args, keys_uicc, keys_euicc)
+ 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 = {'rcpc_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 scc property contains the SimCardCommands object may be used to send APDUs, retrieve the ATR, or even more
+ # complex tasks like selecting a file (see also pysim.commands)
+ scc = 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, hdlr: RcpmCmdSrvConnHdlr, cmd_args: Namespace, keys_uicc: dict, keys_euicc: dict):
+ # The command method (API user) must not access the related RcpmCmdSrvConnHdlr (see below) directly. Only
+ # the resources below may be accessed.
+ self.__hdlr = hdlr
+
+ # Assign properties intended to be used by the command method (API user)
+ self.scc = self.__hdlr.scc
+ self.cmd_args = cmd_args
+ self.keys_uicc = keys_uicc
+ self.keys_euicc = keys_euicc
+
+ def print(self, message: str):
+ """ Print a message on the client side """
+ self.__hdlr.print(message)
+
+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)
+ 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)
+
+ # 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):
+ hdlr = RcpmCmdSrvConnHdlr(module(*args, *kwargs), websocket, RCP_SERVER_TIMEOUT)
+ 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:
+ client = RcpsCltConnHdlr(opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port, module, websocket,
+ RCP_SERVER_TIMEOUT)
+ 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..3e48885a
--- /dev/null
+++ b/contrib/rcp/rcp_server.py
@@ -0,0 +1,361 @@
+#!/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
+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
+import websockets
+from websockets.asyncio.server import serve, ServerConnection
+from rcp_utils import SrvConnHdlr, CltConnHdlr, JsonValidator
+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 argparse_add_card_key_provider_args, init_card_key_provider
+from pySim.card_key_provider import card_key_provider_get_field, card_key_provider_get
+
+# TODO: Logging is fine, however it may also be a good idea to log some higher level events to some sort of journal.
+# We could use OpenObserve for that.
+
+CLIENT_TIMEOUT = 10
+
+log = PySimLogger.get(Path(__file__).stem)
+runtime_state = 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("--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)
+argparse_add_card_key_provider_args(option_parser)
+
+# TODO move those into the RuntimeState?
+rcpc_rx_schema = None
+rcpc_tx_schema = None
+rcpm_ca_ssl_contextssl_context = None
+
+class ModuleRuntimeState:
+ def __init__(self, websocket:ServerConnection, name:str, cmd_descr:list, suitable_for:list, retrieve_keys:dict,
+ addr:str, port:int):
+ self.name = name
+ self.websocket = websocket
+
+ # Run the cmd_descr through argparse to catch malformed arguments early
+ for cmd in cmd_descr:
+ args = deepcopy(cmd['args'])
+ cmd_parser = argparse.ArgumentParser()
+ for arg in args:
+ # TODO: wrap this into a try/catch block and log broken arguments?
+ arg['spec'] = pytype_to_type(arg['spec'])
+ cmd_parser.add_argument(arg['name'], **arg['spec'])
+
+ self.cmd_descr = cmd_descr
+ self.suitable_for = suitable_for
+ self.retrieve_keys = retrieve_keys
+ self.addr = addr
+ self.port = port
+ log.debug("new RCP module context created: '%s'", name)
+
+ def is_suitable(self, suitable_for:dict) -> bool:
+ if suitable_for in self.suitable_for:
+ return True
+ return False
+
+ def describe(self) -> dict:
+ return {'name': self.name,
+ 'cmd_descr': 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):
+ self.module_runtime_states = []
+ 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. Througout 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.
+ """
+
+ 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 (commmandlien help, argument validation) from this information.
+ """
+ rx_json = await self._recv()
+ self.suitable_for = rx_json['rcpc_hello']['suitable_for']
+ modules = runtime_state.modules_find(self.suitable_for)
+ 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)
+ 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)
+ 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 != {'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 decdicated connection to that module and
+ forward instruction/response messages between RCP Client and RCP Module until the procedure is done.
+ """
+
+ # Expect a command from the client
+ rx_json = await self._recv()
+ if rx_json is None:
+ log.debug(str(self) + " -- RCP client has closed the connection, no procedure executed")
+ return
+ command = rx_json['rcpc_command']
+
+ # Pick the matching RCP Module
+ module = runtime_state.module_find(self.suitable_for, command['cmd'])
+
+ # Retrieve keys (if module requires them)
+ keys = {}
+ if module.retrieve_keys['uicc']:
+ iccid = await self._read_iccid()
+ keys_uicc = card_key_provider_get(module.retrieve_keys['uicc'], 'ICCID', iccid)
+ keys['uicc'] = key_value_pairs_from_dict(keys_uicc, keylabel='key', valuelabel='value')
+ else:
+ keys['uicc'] = []
+ if module.retrieve_keys['euicc']:
+ eid = await self._read_eid()
+ keys_euicc = card_key_provider_get(module.retrieve_keys['euicc'], 'EID', eid)
+ keys['euicc'] = key_value_pairs_from_dict(keys_euicc, keylabel='key', valuelabel='value')
+ else:
+ keys['euicc'] = []
+ command['keys'] = keys
+
+ # Resetting card to ensure the card is in a defined state
+ await self._reset()
+
+ # Transparently forward 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=rcpm_ca_ssl_context) as websocket:
+ module_client = RcpmCltConnHdlr(websocket, CLIENT_TIMEOUT)
+ rx_json = {'rcpc_command' : command}
+ while(True):
+ module_rx_json = await module_client._transact(rx_json)
+ await self._send(module_rx_json)
+ if 'rcpc_goodbye' in module_rx_json:
+ log.info(str(self) + " -- command execution done, rc: %d" % module_rx_json['rcpc_goodbye'])
+ break
+ rx_json = await self._recv()
+ await module_client.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 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': {}}
+ 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__()
+
+async def rcpc_conn_hdlr(websocket: ServerConnection):
+ # TODO: Implement some sort of rate limit to protect against DoS. We may count the requests for each requesting
+ # IP address and reject the connection once a certain threshold is reached. (we plan to use the CardKeyProvider
+ # together with a database)
+ try:
+ json_validator = JsonValidator(rcpc_rx_schema, rcpc_tx_schema)
+ hdlr = RcpcSrvConnHdlr(websocket, CLIENT_TIMEOUT, json_validator)
+ await hdlr.describe()
+ await hdlr.procedure()
+ await hdlr.close()
+ except:
+ backtrace("RCPC connection handler")
+
+async def rcpm_conn_hdlr(websocket: ServerConnection):
+ try:
+ hdlr = RcpmSrvConnHdlr(websocket, CLIENT_TIMEOUT)
+ 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)
+ runtime_state = RuntimeState()
+
+ # TODO: Modularize the JSON schemas. We already repeat ourselves with multiple definitions of the ATR fields.
+ rcpc_rx_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpc_rx_schema.json"))
+ rcpc_tx_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpc_tx_schema.json"))
+
+ # 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
+ init_card_key_provider(opts)
+
+ # Start RCP server
+ 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..fcdac1a2
--- /dev/null
+++ b/contrib/rcp/rcp_utils.py
@@ -0,0 +1,254 @@
+#!/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 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)
+
+# TODO: Might be helpful for others as well, move this to pySim.utils?
+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<---------------------")
+
+# TODO: Might be helpful for others as well, move this to pySim.utils?
+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
+
+# TODO: Might be helpful for others as well, move this to pySim.utils?
+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 ocurrence 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.info("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.info("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 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):
+ self.websocket = websocket
+ self.timeout = timeout
+ self.json_validator = json_validator
+ log.debug(str(self) + " -- new handler, timeout: %d sec.", self.timeout)
+
+ def _log_recv_peer(self, rx_json_str: str):
+ peer = "%s:%d<-%s:%d" % (self.websocket.local_address[0],
+ self.websocket.local_address[1],
+ self.websocket.remote_address[0],
+ self.websocket.remote_address[1])
+ log.debug(str(self) + " -- RX(%s): %s", peer, rx_json_str)
+
+ def _log_send_peer(self, tx_json_str: str):
+ peer = "%s:%d->%s:%d" % (self.websocket.local_address[0],
+ self.websocket.local_address[1],
+ self.websocket.remote_address[0],
+ self.websocket.remote_address[1])
+ log.debug(str(self) + " -- TX(%s): %s", peer, 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"""
+ # TODO: we do not have a timeout here (the self.timeout is currently useless). Check if we can do something
+ # about this or if we have to implement some watchdog functionality elsewhere.
+ rx_json_str = 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
+
+ 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_rx_schema.json b/contrib/rcp/rcpc_rx_schema.json
new file mode 100644
index 00000000..4967c24e
--- /dev/null
+++ b/contrib/rcp/rcpc_rx_schema.json
@@ -0,0 +1,69 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "RCP Client RX",
+ "type": "object",
+ "properties": {
+ "rcpc_hello": {
+ "type": "object",
+ "properties": {
+ "suitable_for": {
+ "type": "object",
+ "properties": {
+ "atr": {
+ "type": "string",
+ "pattern": "^[0-9,A-F]{0,66}$"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "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
+ }
+ },
+ "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}$"
+ }
+ },
+ "additionalProperties": false
+ },
+ "atr": {
+ "type": "string",
+ "pattern": "^[0-9,A-F]{0,66}$"
+ },
+ "empty": {
+ "type": "null"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/contrib/rcp/rcpc_tx_schema.json b/contrib/rcp/rcpc_tx_schema.json
new file mode 100644
index 00000000..ed1b9de8
--- /dev/null
+++ b/contrib/rcp/rcpc_tx_schema.json
@@ -0,0 +1,89 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "RCP Client TX",
+ "type": "object",
+ "properties": {
+ "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": {
+ "name": {
+ "type": "string"
+ },
+ "spec": {
+ "type": "object",
+ "properties": {
+ "required" : {
+ "type": "boolean"
+ },
+ "help": {
+ "type": "string"
+ },
+ "action": {
+ "type": "string"
+ },
+ "pytype": {
+ "type": "string"
+ },
+ "default" : {
+ "type": ["string", "integer"]
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "rcpc_instr": {
+ "type": "object",
+ "properties": {
+ "print": {
+ "type": "string"
+ },
+ "reset": {
+ "type": "null"
+ },
+ "c_apdu": {
+ "type": "string",
+ "pattern": "^[0-9,A-F]{0,512}$"
+ }
+ },
+ "additionalProperties": false
+ },
+ "rcpc_goodbye": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+}
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/params.cfg b/contrib/rcp/usage_example/params.cfg
new file mode 100644
index 00000000..3866bf9f
--- /dev/null
+++ b/contrib/rcp/usage_example/params.cfg
@@ -0,0 +1,36 @@
+# PYSIM_DIR passed to all components
+PYSIM_DIR=../../../ # Points to the psyim top directory
+
+# Verbosity switch passed to all components (comment-out to disable verbode mode)
+#VERBOSE="--verbose"
+
+# PCSC reader that the RCP Client shall use
+PCSC_READER=0
+
+# Since RCP Modules are custom implementations, they will most likely reside
+# in a dedicated directory. This directory is passed together with PYSIM_DIR
+# via PYTHONPATH to the module.
+RCP_DIR=../
+
+# CA of the certificates used in this example
+CA_CERT="./certs/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="./certs/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="./certs/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="./certs/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..e4c63012
--- /dev/null
+++ b/contrib/rcp/usage_example/rcp_module.py
@@ -0,0 +1,86 @@
+#!/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 rcp_module_utils import rcpm_setup_argparse, rcpm_run_module, RcpModule, RcpmCmdSrvConnHdlr
+
+log = PySimLogger.get(Path(__file__).stem)
+
+class ExmpleModule(RcpModule):
+
+ name = Path(__file__).stem
+ 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"},
+ }
+ ]}
+ ]
+ suitable_for = [{"atr" : "3b9f96803f87828031e073fe211f574543753130136502"}]
+
+ def cmd_reset(self, hdlr: RcpmCmdSrvConnHdlr) -> int:
+ hdlr.print("resetting UICC/eUICC")
+ hdlr.scc.reset_card()
+ hdlr.print("ATR is: %s" % hdlr.scc.get_atr())
+ return 0
+
+ def cmd_read_binary(self, hdlr: RcpmCmdSrvConnHdlr) -> int:
+ fid = hdlr.cmd_args.fid
+ hdlr.print("reading transparent file: %s" % fid)
+ (res, _) = hdlr.scc.read_binary(fid)
+ hdlr.print("file content is: %s" % res)
+ return 0
+
+ def cmd_read_record(self, hdlr: RcpmCmdSrvConnHdlr) -> int:
+ fid = hdlr.cmd_args.fid
+ record = hdlr.cmd_args.record
+ hdlr.print("reading linear-fixed file: %s" % fid)
+ (res, _) = hdlr.scc.read_record(fid, record)
+ hdlr.print("file content is: %s" % res)
+ return 0
+
+if __name__ == '__main__':
+ option_parser = rcpm_setup_argparse("Example Module")
+ opts = option_parser.parse_args()
+ rcpm_run_module(opts, ExmpleModule)
diff --git a/contrib/rcp/usage_example/readme.txt b/contrib/rcp/usage_example/readme.txt
new file mode 100644
index 00000000..e8cb709f
--- /dev/null
+++ b/contrib/rcp/usage_example/readme.txt
@@ -0,0 +1,14 @@
+How to try:
+
+Go to the directory that contains the usage example:
+cd pysim/contrib/rcp/usage_example
+
+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..d4e6b985
--- /dev/null
+++ b/contrib/rcp/usage_example/run_rcp_client_cmd.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+. ./params.cfg
+
+set -x
+
+PYTHONPATH=$PYSIM_DIR ../rcp_client.py $VERBOSE \
+ --uri $RCPC_SERVER_URI\
+ --ca-cert $CA_CERT \
+ -p $PCSC_READER \
+ rcp_module_reset
+
+PYTHONPATH=$PYSIM_DIR ../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 ../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
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..bb0ee80f
--- /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 ../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..43804d19
--- /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 ../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..cac0fd7a
--- /dev/null
+++ b/contrib/rcp/usage_example/run_rcp_client_help_cmd_specific.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+. ./params.cfg
+
+set -x
+
+PYTHONPATH=$PYSIM_DIR ../rcp_client.py $VERBOSE \
+ --uri $RCPC_SERVER_URI \
+ --ca-cert $CA_CERT \
+ -p $PCSC_READER \
+ rcp_module_reset --help
+
+PYTHONPATH=$PYSIM_DIR ../rcp_client.py $VERBOSE \
+ --uri $RCPC_SERVER_URI \
+ --ca-cert $CA_CERT \
+ -p $PCSC_READER \
+ rcp_module_read_binary --help
+
+PYTHONPATH=$PYSIM_DIR ../rcp_client.py $VERBOSE \
+ --uri $RCPC_SERVER_URI \
+ --ca-cert $CA_CERT \
+ -p $PCSC_READER \
+ rcp_module_read_record --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..c89ff4cd
--- /dev/null
+++ b/contrib/rcp/usage_example/start_rcp_module.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+. ./params.cfg
+
+set -x
+PYTHONPATH=$PYSIM_DIR:$RCP_DIR ./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
+
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..f41b1d7b
--- /dev/null
+++ b/contrib/rcp/usage_example/start_rcp_server.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+. ./params.cfg
+
+set -x
+PYTHONPATH=$PYSIM_DIR ../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
+