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 +