From d9eef6fce71d737f15463bfdbcbf35016996346d Mon Sep 17 00:00:00 2001 From: Philipp Maier Date: Thu, 9 Apr 2026 14:09:52 +0200 Subject: [PATCH] WIP: Remote Card Procedure Framework Problem: When UICC/eUICC cards are deployed into the field it is often difficult to perform modifications to those cards. One important factor that makes after-deployment modifications often difficult is that the key material needed to perform the task must not be handed to the card holder due to security requirements. The presented Remote Card Procedure Framework solves this problem. It provides a so called Remote Card Procedure Client (RCPC), which is a lightwight software client which can be run by the card holder on the remote machine. With the RCPC, the card holder can access a so called Remote Card Procedure Server (RCPC), to which so called Remote Card Procedure Modules (RCPM) can subscribe and publish their functionality. With the RCPC, the card holder can browse the functionality offered by those connected modules and eventually the card holder may execute a certain procedure by passing a command to the RCPS. When a procedure is carried out, the RCPS automatically retrieves the required key material from a database or CSV file and passes those keys on to the selected RCPM. The RCPM can then use the key material to establish a secure channel to carry out the procedure. The procedure is then protected by a secure channel and the key material is never disclosed towards the card holder on the remote end. The framework is desinged in such a way that existing pySim APIs and functions can be used from the RCPM API user code. Also only minimal boilerplate code is required. The implementation also ships with a comprehensive example. Related: SYS#6959 --- contrib/rcp/rcp_client.py | 208 +++++++++ contrib/rcp/rcp_module_utils.py | 405 ++++++++++++++++ contrib/rcp/rcp_server.py | 437 ++++++++++++++++++ contrib/rcp/rcp_utils.py | 254 ++++++++++ contrib/rcp/rcpc_to_rcps_schema.json | 85 ++++ contrib/rcp/rcpm_to_rcps_schema.json | 116 +++++ contrib/rcp/rcpmcs_to_rcps_schema.json | 36 ++ contrib/rcp/rcps_to_rcpc_schema.json | 108 +++++ contrib/rcp/rcps_to_rcpm_schema.json | 14 + contrib/rcp/rcps_to_rcpmcs_schema.json | 106 +++++ contrib/rcp/usage_example/card_data.csv | 2 + contrib/rcp/usage_example/card_data.csv.encr | 2 + .../certs/example_ssl_rcp_ca_cert.crt | 20 + .../certs/example_ssl_rcpc_rcps_cert.pem | 115 +++++ .../certs/example_ssl_rcpm_rcps_cert.pem | 115 +++++ .../certs/example_ssl_rcps_rcpm_cert.pem | 115 +++++ contrib/rcp/usage_example/certs/make_certs.sh | 49 ++ .../rcp/usage_example/encrypt_card_data.sh | 8 + contrib/rcp/usage_example/params.cfg | 40 ++ contrib/rcp/usage_example/rcp_module.py | 126 +++++ contrib/rcp/usage_example/readme.txt | 16 + contrib/rcp/usage_example/run_rcp_client.sh | 29 ++ .../rcp/usage_example/run_rcp_client_cmd.sh | 28 ++ .../rcp/usage_example/run_rcp_client_help.sh | 6 + .../usage_example/run_rcp_client_help_cmd.sh | 9 + .../run_rcp_client_help_cmd_specific.sh | 28 ++ contrib/rcp/usage_example/start_rcp_module.sh | 14 + contrib/rcp/usage_example/start_rcp_server.sh | 13 + 28 files changed, 2504 insertions(+) create mode 100755 contrib/rcp/rcp_client.py create mode 100644 contrib/rcp/rcp_module_utils.py create mode 100755 contrib/rcp/rcp_server.py create mode 100644 contrib/rcp/rcp_utils.py create mode 100644 contrib/rcp/rcpc_to_rcps_schema.json create mode 100644 contrib/rcp/rcpm_to_rcps_schema.json create mode 100644 contrib/rcp/rcpmcs_to_rcps_schema.json create mode 100644 contrib/rcp/rcps_to_rcpc_schema.json create mode 100644 contrib/rcp/rcps_to_rcpm_schema.json create mode 100644 contrib/rcp/rcps_to_rcpmcs_schema.json create mode 100644 contrib/rcp/usage_example/card_data.csv create mode 100644 contrib/rcp/usage_example/card_data.csv.encr create mode 100644 contrib/rcp/usage_example/certs/example_ssl_rcp_ca_cert.crt create mode 100644 contrib/rcp/usage_example/certs/example_ssl_rcpc_rcps_cert.pem create mode 100644 contrib/rcp/usage_example/certs/example_ssl_rcpm_rcps_cert.pem create mode 100644 contrib/rcp/usage_example/certs/example_ssl_rcps_rcpm_cert.pem create mode 100755 contrib/rcp/usage_example/certs/make_certs.sh create mode 100755 contrib/rcp/usage_example/encrypt_card_data.sh create mode 100644 contrib/rcp/usage_example/params.cfg create mode 100755 contrib/rcp/usage_example/rcp_module.py create mode 100644 contrib/rcp/usage_example/readme.txt create mode 100755 contrib/rcp/usage_example/run_rcp_client.sh create mode 100755 contrib/rcp/usage_example/run_rcp_client_cmd.sh create mode 100755 contrib/rcp/usage_example/run_rcp_client_help.sh create mode 100755 contrib/rcp/usage_example/run_rcp_client_help_cmd.sh create mode 100755 contrib/rcp/usage_example/run_rcp_client_help_cmd_specific.sh create mode 100755 contrib/rcp/usage_example/start_rcp_module.sh create mode 100755 contrib/rcp/usage_example/start_rcp_server.sh diff --git a/contrib/rcp/rcp_client.py b/contrib/rcp/rcp_client.py new file mode 100755 index 00000000..33bb19be --- /dev/null +++ b/contrib/rcp/rcp_client.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 + +# (C) 2026 by sysmocom - s.f.m.c. GmbH +# All Rights Reserved +# +# Author: Philipp Maier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os +import websockets +import asyncio +import argparse +import logging +from copy import deepcopy +from pathlib import Path +from pySim.log import PySimLogger +from rcp_utils import CltConnHdlr, backtrace, pytype_to_type, load_ca_cert, load_json_schema, JsonValidator +from pySim.transport import init_reader, argparse_add_reader_args, LinkBase + +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: + rcpc_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpc_to_rcps_schema.json")) + rcps_to_rcpc_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpc_schema.json")) + json_validator = JsonValidator(rcps_to_rcpc_schema, rcpc_to_rcps_schema) + client = RcpcCltConnHdlr(sl, websocket, SERVER_TIMEOUT, json_validator) + + # 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..f260de1c --- /dev/null +++ b/contrib/rcp/rcp_module_utils.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python3 + +# (C) 2026 by sysmocom - s.f.m.c. GmbH +# All Rights Reserved +# +# Author: Philipp Maier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import abc +import os +import argparse +import logging +import threading +import asyncio +import websockets +from argparse import Namespace +from copy import deepcopy +from pathlib import Path +from typing import Optional +from osmocom.utils import Hexstr, is_hexstr +from pySim.utils import ResTuple +from pySim.transport import LinkBase +from pySim.commands import SimCardCommands +from pySim.log import PySimLogger +from rcp_utils import SrvSyncConnHdlr, CltConnHdlr, backtrace, pytype_to_type, load_server_cert, load_ca_cert +from rcp_utils import dict_from_key_value_pairs, load_json_schema, JsonValidator +from websockets.sync.server import serve, ServerConnection +from pySim.app import init_card +from pySim.runtime import RuntimeState +from pySim.cards import CardBase +from pySim.card_key_provider import CardKeyFieldCryptor + +# Response timeout towards the RCP Server (includes RCP Client latency) +RCP_SERVER_TIMEOUT = 30 # sec. + +log = PySimLogger.get(Path(__file__).stem) + +class RcpsSimLink(LinkBase): + """ + pySim: Transport Link for RCPM (Remote Card Procedure Module) + This is a 'headless' transport link implementation that can only be used from an RCPM module. It merely serves as + an adapter between the pySim transport API and the RCPM command server connection handler. + """ + + name = 'RCPM' + + def __init__(self, conn_hdlr: SrvSyncConnHdlr, **kwargs): + self.conn_hdlr = conn_hdlr + self._atr = None + super().__init__(**kwargs) + + def __str__(self) -> str: + return "rcpm:" + str(self.conn_hdlr) + + def _send_apdu(self, apdu: Hexstr) -> ResTuple: + tx_json = {'rcps_instr': {'c_apdu' : apdu.upper()}} + rx_json = self.conn_hdlr._transact(tx_json) + data = rx_json['rcps_result']['r_apdu']['data'] + sw = rx_json['rcps_result']['r_apdu']['sw'] + return data, sw + + def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False): + # In this setting, we do not have/cannot to wait for a card since we are not the entity that handles the + # direct connection to the card. When the procedure begins, we assume that the remote end already has set up + # a connection to the card and made it ready to perform operations on it. + pass + + def connect(self): + # In this setting, we do not have/cannot to connect because we are not the entity that handles the direct + # connection to the card. The connection is established by the remote end. + pass + + def get_atr(self) -> Hexstr: + return self._atr + + def disconnect(self): + # In this setting, we do not have/cannot disconnect because we are not the 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 = {'rcps_instr': {'reset' : None}} + rx_json = self.conn_hdlr._transact(tx_json) + self._atr = rx_json['rcps_result']['atr'] + return 1 + +class RcpsCltConnHdlr(CltConnHdlr): + """ + The RCP Server client handler is used to connect to the RCP Server when RCP Module is started. The connection is + kept alive until the RCP Module is terminated. This connection is used to exchange management data with the RCP + Server. + """ + + def __init__(self, cmd_srv_addr: str, cmd_srv_port: int, module, *args, **kwargs): + self.cmd_srv_addr = cmd_srv_addr + self.cmd_srv_port = cmd_srv_port + self.module = module + super().__init__(*args, **kwargs) + + async def describe(self): + """ + Send a detailed description about this RCP Module to the RCP Server. This is also the initial message that + the RCP Server expects when an RCP Module connects. + """ + + # The rules (dict) in suitable_for (array of dict) may contain hexstrings. Here we go through those rules + # and convert those hexstrings to uppercase, since this is the standard we have set for the JSON messages. + suitable_for = [] + for rule in self.module.suitable_for: + rule_filtered = {} + for k in rule: + if is_hexstr(rule[k]): + rule_filtered[k] = rule[k].upper() + else: + rule_filtered[k] = rule[k] + suitable_for.append(rule_filtered) + + # Publish RCP Module description on the RCP server + tx_json = {'rcpm_hello': + {'name' : self.module.name, + 'cmd_descr' : self.module.cmd_descr, + 'suitable_for' : suitable_for, + 'addr' : self.cmd_srv_addr, + 'port' : self.cmd_srv_port + } + } + rx_json = await self._transact(tx_json) + if 'rcpm_welcome' not in rx_json: + raise ValueError("description not accepted by RCP Server") + +class RcpModule(abc.ABC): + """ + Base class to implement to derive a concrete 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, consisting of + # 'name', 'help', and 'args' is is directly passed to agparse on the client side. + # + # In addition to that, the API user may specify which keys the RCP Server shall retrieve before a command is + # executed. This is done via the 'get_keys' field. This field is optional and has the form of a dict with + # two optional fields 'uicc' and 'euicc'. The value part of both fields is a list of strings which name the + # columns that are passed to the CardKeyProvider for lookup. When the 'uicc' field is set, then the RCP Server + # will automatically request the ICCID from the card and do the lookup. When the 'euicc' field is set, the RCP + # Server will do the same with the EID. It is possible to mix both fields to request keys for the eUICC and the + # currently activated eSIM profile at the same time. However, this may be a very rare cornercase. + # + # Example: + # cmd_descr = [{'name' : 'reset', + # 'help': 'reset the card', + # 'args' : []}, + # {'name' : 'read_binary', + # 'help': 'read binary data from a transparent file.', + # 'args' : [{ 'name' : '--fid', + # 'spec' : {'required' : True, + # 'help' : 'File identifier to of the file to read', + # 'action' : 'append', + # 'pytype' : 'str'}, + # } + # ]}, + # {'name' : 'unlock_aram', + # 'help': 'unlock a locked ARA-M applet on a sysmoISIM-SJA5', + # 'args' : [], + # 'get_keys' : {'uicc' : ['KIC', 'KID', 'KIK']}} + # ] + cmd_descr = [] + + # Card properties to determine if this module is suitable for a specific card type or card types. The RCP Server + # will match those properties against user requests to determine which module provides useful services to the + # user's card. + # + # Example: [{"atr" : "3b9f96803f87828031e073fe211f574543753130136502"}] + suitable_for = [] + + # In addition the above, the derived class must implement command methods for each command that is defined in the + # command description (see above). Each command method must begin with the prefix "cmd_" followed by the command + # name used in the command description. A command method must have the form as shown in the example shown below. + # Each method should return an integer value which will become the final return code of the RCP client program. + # + # Args: + # hdlr: RcpModuleHdlr object, this object is provided by the RcpmCmdSrvConnHdlr object, which calls + # the command method of the module. Through the RcpModuleHdlr object, the API user gets access + # to special service methods (e.g. print) and other required properties (e.g. the SimCardCommands + # objects, key material and others (see RcpModuleHdlr). + # + # Example: + # def cmd_reset(self, hdlr: RcpModuleHdlr) -> int: ... + # def cmd_read_binary(self, hdlr: RcpModuleHdlr) -> int: ... + # def cmd_unlock_aram(self, hdlr: RcpModuleHdlr) -> int: ... + +class RcpmCmdSrvConnHdlr(SrvSyncConnHdlr): + """ + The RCP Module command server connection handler is used to handle dedicated connections from the RCP Server. Those + dedicated connections are technically transparent connections between the RCP Client and the RCP Module (this). The + RCP Server merely acts as a proxy at that point. + """ + + def __init__(self, module: RcpModule, field_cryptor: CardKeyFieldCryptor, *args, **kwargs): + SrvSyncConnHdlr.__init__(self, *args, *kwargs) + self.module = module + self.crypt = field_cryptor + + def _parse_cmd_argv(self, cmd_suffix: str, cmd_argv: list[str]) -> Namespace: + """ Parse (and validate) the received argument vector """ + # Use the cmd_descr of the module to create a (temporary) argument parser for the received argument vector. + cmd_parser = argparse.ArgumentParser() + for cmd in self.module.cmd_descr: + if cmd['name'] == cmd_suffix: + args = deepcopy(cmd['args']) + for arg in args: + arg['spec'] = pytype_to_type(arg['spec']) + cmd_parser.add_argument(arg['name'], **arg['spec']) + + # Parse the arguments and return the parsed Namespace object. + try: + return cmd_parser.parse_args(cmd_argv) + except SystemExit: + raise ValueError("unable to parse arguments: %s", str(cmd_argv), ) + + def print(self, message: str): + """ Print a message on the client side """ + log.info(str(self) + " -- %s" % message) + tx_json = {'rcps_instr': {'print' : message}} + rx_json = self._transact(tx_json) + if rx_json != {'rcps_result': {'empty' : None}}: + raise ValueError("unexpected response from RCP Client: %s", rx_json) + + def procedure(self): + """ Receive and process a command from the RCP Client (via RCP Server) """ + + # Receive the command request. + rx_json = self._recv() + cmd = rx_json['rcps_command']['cmd'] + cmd_argv = rx_json['rcps_command']['cmd_argv'] + keys = rx_json['rcps_command'].get('keys') + + log.info(str(self) + " -- executing command: %s %s", cmd, " ".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) + + # Setup a pySim RuntimeState, CardBase and a RuntimeLchan. + rs, card = init_card(RcpsSimLink(self)) + + # Hand over control to the command method provided by the specific module implementation. + rcp_module_hdlr = RcpModuleHdlr(self.print, rs, card, cmd_args, keys, self.crypt) + rs.reset() + try: + rc = getattr(self.module, cmd_method)(rcp_module_hdlr) + except Exception as e: + backtrace("command method") + rc = 1 # general error + + except Exception as e: + backtrace("command parsing") + rc = 126 # cannot execute + + # The prodedure is done, send "goodbye" message. + log.info(str(self) + " -- command execution done, rc: %d" % rc) + tx_json = {'rcps_goodbye': rc} + self._send(tx_json) + +class RcpModuleHdlr(): + """ + RCP Module handler class. This class is used by the RcpmCmdSrvConnHdlr to create the handler RcpModuleHdlr object + (hdlr), which is is passed to the command method. The RcpModuleHdlr gives the API user access to resources he can + use carry out the command. + """ + + # The RuntimeState (rs), the CardBase (card) and the RuntimeLchan (lchan) are the three major objects through which + # an API user may interact with the UICC/eUICC on the other remote end. Those objects have the same objectives as + # in pySim-shell.py, with lchan representing the currently selected lchan (set to self.rs.lchan[0] by default, API + # users may change the reference to a different lchan) + rs = None + card = None + lchan = None + + # The cmd_args property contains the parsed command arguments which were passed by the end-user to the RCP Client. + # The arguments are already parsed and validated against the cmd_dscr property of the RcpModule. The arguments are + # in the form of a Namespace object and can be accessed like any argparse output. However, since the arguments + # contain user input, some caution is required. + cmd_args = None + + # In case the retrieve_uicc_keys property of the RcpModule is used retrieve UICC key material, this property will + # contain the key material in the form of a dictionary. The format is similar to the return value of + # card_key_provider_get() (see also pySim.card_key_provider). + keys_uicc = {} + + # Same as self.keys_uicc, but contains eUICC related key material in case requested using retrieve_uicc_keys. + keys_euicc = {} + + def __init__(self, print: callable, rs: RuntimeState, card: CardBase, cmd_args: Namespace, + keys: dict, field_cryptor: CardKeyFieldCryptor): + self.print = print + self.rs = rs + self.card = card + self.lchan = self.rs.lchan[0] + self.cmd_args = cmd_args + if keys: + if 'uicc' in keys: + self.keys_uicc = dict_from_key_value_pairs(keys['uicc'], keylabel='key', valuelabel='value') + for key in self.keys_uicc.keys(): + self.keys_uicc[key] = field_cryptor.decrypt_field(key, self.keys_uicc.get(key)) + if 'euicc' in keys: + self.keys_euicc = dict_from_key_value_pairs(keys['euicc'], keylabel='key', valuelabel='value') + for key in self.keys_euicc.keys(): + self.keys_euicc[key] = field_cryptor.decrypt_field(key, self.keys_euicc.get(key)) + +def rcpm_setup_argparse(description: str): + """Create argument parser and add the basic arguments all RCP Modules should have""" + + option_parser = argparse.ArgumentParser(description='RCP Module: ' + description, + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False) + option_parser.add_argument("--uri", help="URI of the RCP-Server", required=True) + option_parser.add_argument("--rcps-ca-cert", help="SSL/TLS CA-Certificate of the RCP-Server", required=True) + option_parser.add_argument("--rcpm-cmd-server-addr", help="Local Host/IP to bind RCP-Module-Command-Server to", + required=True) + option_parser.add_argument("--rcpm-cmd-server-port", help="Local TCP port to bind RCP-Module-Command-Server to", + required=True, type=int) + option_parser.add_argument("--rcpm-cmd-server-cert", help="SSL/TLS Certificate of the RCP-Module-Command-Server", + required=True) + CardKeyFieldCryptor.argparse_add_args(option_parser) + return option_parser + +def rcpm_run_module(opts: Namespace, module: RcpModule, *args, **kwargs): + + PySimLogger.setup(print, {logging.WARN: "\033[33m", logging.DEBUG: "\033[90m"}, opts.verbose) + log.info("RCP Module startup: %s", module.name) + log.debug("Main process ID: %d", os.getpid()) + + # Load SSL/TLS certificates. + rcpm_cmd_ssl_context = load_server_cert("RCPM Command Server", opts.rcpm_cmd_server_cert) + ssl_context = load_ca_cert("RCPM Server Client", opts.rcps_ca_cert) + + # Load JSON schema for message validation between RCP Server and RCP Module (this process) + rcpm_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpm_to_rcps_schema.json")) + rcps_to_rcpm_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpm_schema.json")) + + # Load JSON schema for message validation between RCP Server and RCP Module Command Server (this process) + rcpmcs_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpmcs_to_rcps_schema.json")) + rcps_to_rcpmcs_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpmcs_schema.json")) + + # Start local RCP Client Command Server. + log.info("RCPC command server at: %s:%d" % (opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port)) + def rcpm_cmd_conn_hdlr(websocket: ServerConnection): + json_validator = JsonValidator(rcps_to_rcpmcs_schema, rcpmcs_to_rcps_schema) + transport_keys = CardKeyFieldCryptor.transport_keys_from_opts(opts) + field_cryptor = CardKeyFieldCryptor(transport_keys) + hdlr = RcpmCmdSrvConnHdlr(module(*args, *kwargs), field_cryptor, websocket, RCP_SERVER_TIMEOUT, json_validator) + hdlr.procedure() + hdlr.close() + + server = serve(rcpm_cmd_conn_hdlr, opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port, ssl=rcpm_cmd_ssl_context) + def rcpm_cmd_server(): + log.debug("RCPC command server thread ID: %d", threading.get_native_id()) + server.serve_forever() + rcpm_cmd_server_thread = threading.Thread(target = rcpm_cmd_server) + rcpm_cmd_server_thread.start() + + # Connect to RCP Server and publish module description. + async def rcps_client(): + async with websockets.connect(opts.uri, ping_timeout=10.0, ping_interval=1.0, ssl=ssl_context) as websocket: + json_validator = JsonValidator(rcps_to_rcpm_schema, rcpm_to_rcps_schema) + client = RcpsCltConnHdlr(opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port, module, websocket, + RCP_SERVER_TIMEOUT, json_validator) + await client.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..4cc689f4 --- /dev/null +++ b/contrib/rcp/rcp_server.py @@ -0,0 +1,437 @@ +#!/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) + +class ModuleRuntimeState: + def __init__(self, websocket:ServerConnection, name:str, cmd_descr:list, suitable_for:list, addr:str, port:int): + self.name = name + self.websocket = websocket + + # Run the cmd_descr through argparse to catch malformed argument specifications early + for cmd in cmd_descr: + args = deepcopy(cmd['args']) + cmd_parser = argparse.ArgumentParser() + for arg in args: + try: + arg['spec'] = pytype_to_type(arg['spec']) + cmd_parser.add_argument(arg['name'], **arg['spec']) + except: + raise ValueError("invalid argument spec %s -- check RCP Module" % str(arg)) + + self.cmd_descr = cmd_descr + self.suitable_for = suitable_for + self.addr = addr + self.port = port + log.debug("new RCP module context created: '%s'", name) + + def is_suitable(self, suitable_for:dict) -> bool: + """Check if this module is 'suitable_for' a specific card""" + if suitable_for in self.suitable_for: + return True + return False + + def describe(self) -> dict: + """Describe this module towards the RCP Client""" + + # The command description sent by the RCP Module also includes fields that are intended to be seen + # only by the RCP Server. Here we set up the command description as it is expected by the RCP Client. + cmd_descr = [] + for descr in self.cmd_descr: + cmd_descr.append({'name' : descr['name'], + 'help' : descr['help'], + 'args' : descr['args']}) + + # Return module description + return {'name': self.name, + 'cmd_descr': cmd_descr} + + def get_cmd_descr(self, cmd: str) -> dict: + """Get the description for a specific command of this module""" + for descr in self.cmd_descr: + if self.name + "_" + descr['name'] == cmd: + return descr + raise ValueError("command %s not found in command description %s" % (cmd_name, str(self.cmd_descr))) + + def __str__(self) -> str: + return self.name + + def __del__(self): + log.debug("RCP module context destroyed: '%s'", self.name) + +class RuntimeState: + def __init__(self, rcpm_ca_ssl_context): + self.module_runtime_states = [] + self.rcpm_ca_ssl_context = rcpm_ca_ssl_context + + # Load JSON schema for message validation between RCP Client and RCP Server (this process) + self.rcpc_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), + "rcpc_to_rcps_schema.json")) + self.rcps_to_rcpc_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), + "rcps_to_rcpc_schema.json")) + + # Load JSON schema for message validation between RCP Module and RCP Server (this process) + self.rcpm_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), + "rcpm_to_rcps_schema.json")) + self.rcps_to_rcpm_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), + "rcps_to_rcpm_schema.json")) + + # Load JSON schema for message validation between RCP Module Command Server and RCP Server (this process) + self.rcpmcs_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), + "rcpmcs_to_rcps_schema.json")) + self.rcps_to_rcpmcs_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), + "rcps_to_rcpmcs_schema.json")) + + log.debug("new runtime context created.") + + def __log_modules_available(self) -> str: + if self.module_runtime_states: + modules_str = "" + for module in self.module_runtime_states: + modules_str += "'" + str(module) + "', " + return "RCP modules available: %s" % modules_str[:-2] + else: + return "RCP modules available: none" + + def module_add(self, module: ModuleRuntimeState): + self.module_runtime_states.append(module) + log.info("new RCP module, %s", self.__log_modules_available()) + + def module_remove(self, websocket:ServerConnection): + for module in self.module_runtime_states: + if module.websocket == websocket: + self.module_runtime_states.remove(module) + log.info("RCP module removed, %s", self.__log_modules_available()) + return + log.warning("cannot remove RCP module, no RCP module associated with RCPC connection: %s:%d, %s" % + (*websocket.remote_address, self.__log_modules_available())) + + def modules_find(self, suitable_for:dict) -> list[dict]: + modules = [] + for module in self.module_runtime_states: + if module.is_suitable(suitable_for): + modules.append(module.describe()) + if modules: + return modules + # It is absolutely tolerable if no suitable RCP module can be found. If this is the case, the client should + # display an empty help screen and exit normally. + log.warning("no suitable RCP module found, %s", self.__log_modules_available()) + return [] + + def module_find(self, suitable_for:dict, cmd:str) -> ModuleRuntimeState: + modules = self.modules_find(suitable_for) + for m in modules: + module_name = m['name'] + cmd_descr = m['cmd_descr'] + for c in cmd_descr: + cmd_name = c['name'] + if module_name + "_" + cmd_name == cmd: + break + for module_runtime_state in self.module_runtime_states: + if module_runtime_state.name == module_name: + return module_runtime_state + # Normally we should find the RCP module. When this method is called, we have already called modules_find + # before because we had to return the command descriptions to the client. If we cannot find the RCP module + # now, the module have been disconnected or the client somehow called a command that does not exist. In any + # case, ending up here means we cannot continue. + raise ValueError("RCP module not found for command: %s, " % (cmd, self.__log_modules_available())) + +class RcpmCltConnHdlr(CltConnHdlr): + """ + The RCP Module client connection handler is the dedicated client that is used by the RCP Client connection handler + to handle the dedicated connection towards the RCP Module (see below) + """ + +class RcpcSrvConnHdlr(SrvConnHdlr): + """ + The RCP Client connection handler takes care of the handling of client requests. 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. + """ + + module_client = None + + 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) + if rx_json is None: + raise ValueError("RCP Client vanished unexpectetly") + data = rx_json['rcpc_result']['r_apdu']['data'] + sw = rx_json['rcpc_result']['r_apdu']['sw'] + return data, sw + + async def _reset(self) -> Hexstr: + """Private low level method to reset the UICC/eUICC""" + tx_json = {'rcpc_instr': {'reset' : None}} + rx_json = await self._transact(tx_json) + if rx_json is None: + raise ValueError("RCP Client vanished unexpectetly") + return rx_json['rcpc_result']['atr'] + + async def _read_iccid(self) -> Hexstr: + """Private low level method to read the EID from an UICC (or eSIM)""" + data, sw = await self._transact_apdu("00A40000022FE200") + if sw != "9000": + raise ValueError("Unable to select EF.ICCID, sw: %s, " % sw) + data, sw = await self._transact_apdu("00B000000A") + if sw != "9000": + raise ValueError("Unable to read EF.ICCID, sw: %s, " % sw) + return dec_iccid(data) + + async def _read_eid(self) -> Hexstr: + """Private low level method to read the EID from an eUICC""" + data, sw = await self._transact_apdu("00A4040410A0000005591010FFFFFFFF890000010000") + if sw != "9000": + raise ValueError("Unable to select ISD-R, sw: %s, " % sw) + data, sw = await self._transact_apdu("80E2910006BF3E035C015A00") + if sw != "9000": + raise ValueError("Unable to retrieve EID, sw: %s, " % sw) + return data[10:] + + async def print(self, message: str): + """ Print a message on the client side """ + tx_json = {'rcpc_instr': {'print' : message}} + rx_json = await self._transact(tx_json) + if rx_json is None: + raise ValueError("RCP Client vanished unexpectetly") + 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 the command requires them) + cmd_descr = module.get_cmd_descr(command['cmd']) + get_keys = cmd_descr.get('get_keys') + if get_keys: + keys = {} + get_keys_uicc = get_keys.get('uicc') + if get_keys_uicc: + iccid = await self._read_iccid() + keys_uicc = card_key_provider_get(get_keys_uicc, 'ICCID', iccid) + keys['uicc'] = key_value_pairs_from_dict(keys_uicc, keylabel='key', valuelabel='value') + get_keys_euicc = get_keys.get('euicc') + if get_keys_euicc: + eid = await self._read_eid() + keys_euicc = card_key_provider_get(get_keys_euicc, 'EID', eid) + keys['euicc'] = key_value_pairs_from_dict(keys_euicc, keylabel='key', valuelabel='value') + command['keys'] = keys + + # Resetting card to ensure the card is in a defined state + await self._reset() + + # Create a dedicated connection to the RCP Module and proxy the messages between RCP Client and RCP Module. + module_uri = "wss://%s:%d" % (module.addr, module.port) + log.info(str(self) + " -- executing procedure for command \"%s\" on module \"%s\" at: %s" % + (command['cmd'], module.name, module_uri)) + async with websockets.connect(module_uri, ssl=runtime_state.rcpm_ca_ssl_context) as websocket: + # Create a connection to the RCP Module Command Server + json_validator = JsonValidator(runtime_state.rcpmcs_to_rcps_schema, runtime_state.rcps_to_rcpmcs_schema) + self.module_client = RcpmCltConnHdlr(websocket, CLIENT_TIMEOUT, json_validator) + + # Prepare initial request to be send to the RCP Module Command Server + module_tx_json = {'rcps_command' : command} + + while(True): + # Send request to the RCP Module Command Server + module_rx_json = await self.module_client._transact(module_tx_json) + + # Forward the response to the RCP Client + if 'rcps_instr' in module_rx_json: + client_tx_json = {'rcpc_instr' : module_rx_json['rcps_instr']} + await self._send(client_tx_json) + elif 'rcps_goodbye' in module_rx_json: + log.info(str(self) + " -- command execution done, rc: %d" % module_rx_json['rcps_goodbye']) + client_tx_json = {'rcpc_goodbye' : module_rx_json['rcps_goodbye']} + await self._send(client_tx_json) + break + else: + raise ValueError("Unexpected response from RCP Module: %s" % str(module_rx_json)) + + # Receive the Result from the client, prepare request (module_tx_json) for the next turn + client_rx_json = await self._recv() + if client_rx_json is None: + raise ValueError("RCP client vanished unexpectetly") + if 'rcpc_result' in client_rx_json: + module_tx_json = {'rcps_result' : client_rx_json['rcpc_result']} + else: + raise ValueError("Unexpected result from RCP Client: %s" % str(client_rx_json)) + + async def close(self): + if self.module_client: + await self.module_client.close() + await super().close() + +class RcpmSrvConnHdlr(SrvConnHdlr): + """ + The RCP Module connection handler is responsible to handle connect and disconnect events of RCP Modules. This + connection between the RCP Module and the RCP Server is used for management purposes only. + """ + + async def describe(self): + """ + Receive the module description from an RCP Module. This description will be stored in an internal list until + the module is disconnected from the server. + """ + rx_json = await self._recv() + runtime_state.module_add(module = ModuleRuntimeState(self.websocket, **rx_json['rcpm_hello'])) + tx_json = {'rcpm_welcome': None} + await self._send(tx_json) + + def __del__(self): + """ + Remove RCPM from internal list when the connection is closed (and the handler is deleted) + """ + runtime_state.module_remove(self.websocket) + super().__del__() + +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(runtime_state.rcpc_to_rcps_schema, runtime_state.rcps_to_rcpc_schema) + hdlr = RcpcSrvConnHdlr(websocket, CLIENT_TIMEOUT, json_validator) + except: + backtrace("RCPC connection handler (create)") + # If the handler creation fails, it makes no sense to continue + return + + try: + await hdlr.describe() + await hdlr.procedure() + except: + backtrace("RCPC connection handler (client interaction)") + + try: + await hdlr.close() + except: + backtrace("RCPC connection handler close") + +async def rcpm_conn_hdlr(websocket: ServerConnection): + try: + json_validator = JsonValidator(runtime_state.rcpm_to_rcps_schema, runtime_state.rcps_to_rcpm_schema) + hdlr = RcpmSrvConnHdlr(websocket, CLIENT_TIMEOUT, json_validator) + await hdlr.describe() + await hdlr.close() + except: + backtrace("RCPM connection handler") + +if __name__ == '__main__': + opts = option_parser.parse_args() + PySimLogger.setup(print, {logging.WARN: "\033[33m", logging.DEBUG: "\033[90m"}, opts.verbose) + + # Load SSL/TLS certificates + rcpc_ssl_context = load_server_cert("RCP Client Server", opts.rcpc_server_cert) + rcpm_ssl_context = load_server_cert("RCP Module Server", opts.rcpm_server_cert) + rcpm_ca_ssl_context = load_ca_cert("RCP Module Command Server Client", opts.rcpm_module_ca_cert) + + # Init card key provider for automatic card key retrieval + init_card_key_provider(opts) + + # Start RCP server + runtime_state = RuntimeState(rcpm_ca_ssl_context) + 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..85b0e01d --- /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.local_address = websocket.local_address + self.remote_address = websocket.remote_address + 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.local_address[0], + self.local_address[1], + self.remote_address[0], + self.remote_address[1]) + log.debug(str(self) + " -- RX(%s): %s", peer, rx_json_str) + + def _log_send_peer(self, tx_json_str: str): + peer = "%s:%d->%s:%d" % (self.local_address[0], + self.local_address[1], + self.remote_address[0], + self.remote_address[1]) + log.debug(str(self) + " -- TX(%s): %s", peer, tx_json_str) + + def __str__(self) -> str: + return "%s(%d)" % (type(self).__name__, id(self)) + + def __del__(self): + log.debug(str(self) + " -- closed handler") + +class SrvConnHdlr(ConnHdlr): + """Base class that can be used to create a connection handler for a server""" + + async def _recv(self) -> dict: + """Receive JSON message from client""" + async with asyncio.timeout(self.timeout): + try: + rx_json_str = await self.websocket.recv() + except websockets.exceptions.ConnectionClosedOK: + log.debug(str(self) + " -- no data received, connection is closed") + return None + self._log_recv_peer(rx_json_str) + rx_json = json.loads(rx_json_str) + if self.json_validator: + self.json_validator.valid_rx_json(rx_json) + return rx_json + + async def _send(self, tx_json: dict): + """Send JSON message to client""" + if self.json_validator: + self.json_validator.valid_tx_json(tx_json) + tx_json_str = json.dumps(tx_json) + self._log_send_peer(tx_json_str) + await self.websocket.send(tx_json_str) + + async def _transact(self, tx_json: dict) -> dict: + """Exchange JSON message with client""" + await self._send(tx_json) + return await self._recv() + + async def close(self): + """Wait for a connecion to close normally""" + await self.websocket.wait_closed() + log.debug(str(self) + " -- closed connection") + +class SrvSyncConnHdlr(ConnHdlr): + """Base class that can be used to create a synchronous connection handler for a server""" + + def _recv(self) -> dict: + """Receive JSON message from client""" + rx_json_str = self.websocket.recv(self.timeout) + self._log_recv_peer(rx_json_str) + rx_json = json.loads(rx_json_str) + if self.json_validator: + self.json_validator.valid_rx_json(rx_json) + return rx_json + + def _send(self, tx_json: dict): + """Send JSON message to client""" + if self.json_validator: + self.json_validator.valid_tx_json(tx_json) + tx_json_str = json.dumps(tx_json) + self._log_send_peer(tx_json_str) + self.websocket.send(tx_json_str) + + def _transact(self, tx_json: dict) -> dict: + """Exchange JSON message with client""" + self._send(tx_json) + return self._recv() + + def close(self): + """Close connection normally""" + self.websocket.close() + log.debug(str(self) + " -- closed connection") + +class CltConnHdlr(ConnHdlr): + """Base class that can be used to create a connection handler for a client""" + + async def _transact(self, tx_json: dict) -> dict: + """Exchange JSON message with server""" + if self.json_validator: + self.json_validator.valid_tx_json(tx_json) + tx_json_str = json.dumps(tx_json) + self._log_send_peer(tx_json_str) + async with asyncio.timeout(self.timeout): + await self.websocket.send(tx_json_str) + rx_json_str = await self.websocket.recv() + self._log_recv_peer(rx_json_str) + rx_json = json.loads(rx_json_str); + if self.json_validator: + self.json_validator.valid_rx_json(rx_json) + return rx_json + + async def close(self): + """Close connection normally""" + await self.websocket.close() + log.debug(str(self) + " -- closed connection") + + async def wait_close(self): + """Wait for a connecion to close normally""" + await self.websocket.wait_closed() + log.debug(str(self) + " -- closed connection") diff --git a/contrib/rcp/rcpc_to_rcps_schema.json b/contrib/rcp/rcpc_to_rcps_schema.json new file mode 100644 index 00000000..0503d715 --- /dev/null +++ b/contrib/rcp/rcpc_to_rcps_schema.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RCP Client to RCP Server", + "type": "object", + "properties": { + "rcpc_hello": { + "type": "object", + "properties": { + "suitable_for": { + "type": "object", + "properties": { + "atr": { + "type": "string", + "pattern": "^[0-9,A-F]{0,66}$" + } + }, + "oneOf": [ + { "required": [ "atr" ] } + ], + "additionalProperties": false + } + }, + "required": [ "suitable_for" ], + "additionalProperties": false + }, + "rcpc_command": { + "type": "object", + "properties": { + "cmd": { + "type": "string", + "pattern": "^[0-9,A-Z,a-z,_]{0,40}$" + }, + "cmd_argv": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.{0,512}$" + }, + "maxItems": 255 + } + }, + "required": [ "cmd", "cmd_argv" ], + "additionalProperties": false + }, + "rcpc_result": { + "type": "object", + "properties": { + "r_apdu": { + "type": "object", + "properties": { + "data": { + "type": "string", + "pattern": "^[0-9,A-F]{0,512}$" + }, + "sw": { + "type": "string", + "pattern": "^[0-9,A-F]{0,4}$" + } + }, + "required": [ "data", "sw" ], + "additionalProperties": false + }, + "atr": { + "type": "string", + "pattern": "^[0-9,A-F]{0,66}$" + }, + "empty": { + "type": "null" + } + }, + "oneOf": [ + { "required": [ "r_apdu" ] }, + { "required": [ "atr" ] }, + { "required": [ "empty" ] } + ], + "additionalProperties": false + } + }, + "oneOf": [ + { "required": [ "rcpc_hello" ] }, + { "required": [ "rcpc_command" ] }, + { "required": [ "rcpc_result" ] } + ], + "additionalProperties": false +} diff --git a/contrib/rcp/rcpm_to_rcps_schema.json b/contrib/rcp/rcpm_to_rcps_schema.json new file mode 100644 index 00000000..883849e8 --- /dev/null +++ b/contrib/rcp/rcpm_to_rcps_schema.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RCP Module to RCP Server", + "type": "object", + "properties": { + "rcpm_hello": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "cmd_descr": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "help": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "spec": { + "type": "object", + "properties": { + "required" : { + "type": "boolean" + }, + "help": { + "type": "string" + }, + "action": { + "type": "string" + }, + "pytype": { + "type": "string" + }, + "default" : { + "type": ["string", "integer"] + } + }, + "required": [ "help" ], + "additionalProperties": false + } + }, + "required": [ "name", "spec" ], + "additionalProperties": false + } + }, + "get_keys": { + "type": "object", + "properties": { + "uicc" : { + "type": "array", + "items": { + "type": "string" + } + }, + "euicc" : { + "type": "array", + "items": { + "type": "string" + } + } + }, + "oneOf": [ + { "required": [ "uicc" ] }, + { "required": [ "euicc" ] } + ], + "additionalProperties": false + } + }, + "required": [ "name", "help", "args" ], + "additionalProperties": false + } + }, + "suitable_for": { + "type": "array", + "items": { + "type": "object", + "properties": { + "atr": { + "type": "string", + "pattern": "^[0-9,A-F]{0,66}$" + } + }, + "oneOf": [ + { "required": [ "atr" ] } + ], + "additionalProperties": false + } + }, + "addr": { + "type": "string" + }, + "port": { + "type": "integer" + } + }, + "required": [ "name", "cmd_descr", "suitable_for", "addr", "port" ], + "additionalProperties": false + } + }, + "oneOf": [ + { "required": [ "rcpm_hello" ] } + ], + "additionalProperties": false +} diff --git a/contrib/rcp/rcpmcs_to_rcps_schema.json b/contrib/rcp/rcpmcs_to_rcps_schema.json new file mode 100644 index 00000000..81a46d56 --- /dev/null +++ b/contrib/rcp/rcpmcs_to_rcps_schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RCP Module Command Server to RCP Server", + "type": "object", + "properties": { + "rcps_instr": { + "type": "object", + "properties": { + "print": { + "type": "string" + }, + "reset": { + "type": "null" + }, + "c_apdu": { + "type": "string", + "pattern": "^[0-9,A-F]{0,512}$" + } + }, + "oneOf": [ + { "required": [ "print" ] }, + { "required": [ "reset" ] }, + { "required": [ "c_apdu" ] } + ], + "additionalProperties": false + }, + "rcps_goodbye": { + "type": "integer" + } + }, + "oneOf": [ + { "required": [ "rcps_instr" ] }, + { "required": [ "rcps_goodbye" ] } + ], + "additionalProperties": false +} diff --git a/contrib/rcp/rcps_to_rcpc_schema.json b/contrib/rcp/rcps_to_rcpc_schema.json new file mode 100644 index 00000000..c2a6e20b --- /dev/null +++ b/contrib/rcp/rcps_to_rcpc_schema.json @@ -0,0 +1,108 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RCP Server to RCP Client", + "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": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "spec": { + "type": "object", + "properties": { + "required" : { + "type": "boolean" + }, + "help": { + "type": "string" + }, + "action": { + "type": "string" + }, + "pytype": { + "type": "string" + }, + "default" : { + "type": ["string", "integer"] + } + }, + "required": [ "help" ], + "additionalProperties": false + } + }, + "required": [ "name", "spec" ], + "additionalProperties": false + } + } + }, + "required": [ "name", "help", "args" ], + "additionalProperties": false + } + } + }, + "required": [ "name", "cmd_descr" ], + "additionalProperties": false + } + } + }, + "required": [ "module_descr" ], + "additionalProperties": false + }, + "rcpc_instr": { + "type": "object", + "properties": { + "print": { + "type": "string" + }, + "reset": { + "type": "null" + }, + "c_apdu": { + "type": "string", + "pattern": "^[0-9,A-F]{0,512}$" + } + }, + "oneOf": [ + { "required": [ "print" ] }, + { "required": [ "reset" ] }, + { "required": [ "c_apdu" ] } + ], + "additionalProperties": false + }, + "rcpc_goodbye": { + "type": "integer" + } + }, + "oneOf": [ + { "required": [ "rcpc_welcome" ] }, + { "required": [ "rcpc_instr" ] }, + { "required": [ "rcpc_goodbye" ] } + ], + "additionalProperties": false +} diff --git a/contrib/rcp/rcps_to_rcpm_schema.json b/contrib/rcp/rcps_to_rcpm_schema.json new file mode 100644 index 00000000..75767864 --- /dev/null +++ b/contrib/rcp/rcps_to_rcpm_schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RCP Server to RCP Module", + "type": "object", + "properties": { + "rcpm_welcome": { + "type": "null" + } + }, + "oneOf": [ + { "required": [ "rcpm_welcome" ] } + ], + "additionalProperties": false +} diff --git a/contrib/rcp/rcps_to_rcpmcs_schema.json b/contrib/rcp/rcps_to_rcpmcs_schema.json new file mode 100644 index 00000000..e3615242 --- /dev/null +++ b/contrib/rcp/rcps_to_rcpmcs_schema.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RCP Server to RCP Module Command Server", + "type": "object", + "properties": { + "rcps_command": { + "type": "object", + "properties": { + "cmd": { + "type": "string", + "pattern": "^[0-9,A-Z,a-z,_]{0,40}$" + }, + "cmd_argv": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.{0,512}$" + }, + "maxItems": 255 + }, + "keys" : { + "type": "object", + "properties": { + "uicc" : { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ "key", "value" ], + "additionalProperties": false + } + }, + "euicc" : { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ "key", "value" ], + "additionalProperties": false + } + } + }, + "oneOf": [ + { "required": [ "uicc" ] }, + { "required": [ "euicc" ] } + ], + "additionalProperties": false + } + }, + "required": [ "cmd", "cmd_argv" ], + "additionalProperties": false + }, + "rcps_result": { + "type": "object", + "properties": { + "r_apdu": { + "type": "object", + "properties": { + "data": { + "type": "string", + "pattern": "^[0-9,A-F]{0,512}$" + }, + "sw": { + "type": "string", + "pattern": "^[0-9,A-F]{0,4}$" + } + }, + "required": [ "data", "sw" ], + "additionalProperties": false + }, + "atr": { + "type": "string", + "pattern": "^[0-9,A-F]{0,66}$" + }, + "empty": { + "type": "null" + } + }, + "oneOf": [ + { "required": [ "r_apdu" ] }, + { "required": [ "atr" ] }, + { "required": [ "empty" ] } + ], + "additionalProperties": false + } + }, + "oneOf": [ + { "required": [ "rcps_command" ] }, + { "required": [ "rcps_result" ] } + ], + "additionalProperties": false +} diff --git a/contrib/rcp/usage_example/card_data.csv b/contrib/rcp/usage_example/card_data.csv new file mode 100644 index 00000000..132b7877 --- /dev/null +++ b/contrib/rcp/usage_example/card_data.csv @@ -0,0 +1,2 @@ +iccid,kic,kid,kik +8949440000001155306,F09C43EE1A0391665CC9F05AF4E0BD10,01981F4A20999F62AF99988007BAF6CA,8F8AEE5CDCC5D361368BC45673D99195 diff --git a/contrib/rcp/usage_example/card_data.csv.encr b/contrib/rcp/usage_example/card_data.csv.encr new file mode 100644 index 00000000..fc55dcce --- /dev/null +++ b/contrib/rcp/usage_example/card_data.csv.encr @@ -0,0 +1,2 @@ +"ICCID","KIC","KID","KIK" +"8949440000001155306","eae46224fa0a4ac1c12cba9d102f1188","3f14b978ddb38c08d832d4e4c2e0639d","9e19db4a5ed5cb8c4f5d96283eab273a" diff --git a/contrib/rcp/usage_example/certs/example_ssl_rcp_ca_cert.crt b/contrib/rcp/usage_example/certs/example_ssl_rcp_ca_cert.crt new file mode 100644 index 00000000..ab656d1f --- /dev/null +++ b/contrib/rcp/usage_example/certs/example_ssl_rcp_ca_cert.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSzCCAjOgAwIBAgIUEv1f0yjVtkr+RNYLItZ33eTJwHMwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjYwNDI5MTIwOTM0WhcNMzYw +NDI2MTIwOTM0WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANXdkSyQlDzuo2cJmnBmFiZpc0V9tYBcNkpZd3Ac +R0WljazKKgXDWNmOcSO7891bi+1HZzz+nDfV0mJY776ScGkTqF43Hzpg9eZakMAx +yC24mT4h+uyRcPWZrBwaQhpiQrvZy4MRyuUB+BEgBSmhoDiuXP44kWiuEJHuzpOq +X6Q2dW8RIeQPDGGK6XPZIQLqx+krxkaqphd/vHgT1/yd7Ol5xxMc4x2UuPaVCj0D +OzslFsbb0Zu77ffCtHOVVnzSCzeEGGx1MPQm6hDVW+KUXXTwke1K55fmFZhu0gKO +HYSEjgPj6X8muDb+GvOAQX3fHmS6KvFS4fwWd2InZ3v2f3cCAwEAAaOBkDCBjTAM +BgNVHRMEBTADAQH/MB0GA1UdDgQWBBS6zY4Dd0pJFrvWLmyjn0vDTFqVqzBRBgNV +HSMESjBIgBS6zY4Dd0pJFrvWLmyjn0vDTFqVq6EapBgwFjEUMBIGA1UEAwwLRWFz +eS1SU0EgQ0GCFBL9X9Mo1bZK/kTWCyLWd93kycBzMAsGA1UdDwQEAwIBBjANBgkq +hkiG9w0BAQsFAAOCAQEAGJUXlbnVhh+xL+pyTyjwtd8nxhUcHzYZl+OT0bkGY9zT +S3NjHkKBbdnEftuYDYqp0uBuGFQ1WIOKiM3rp4IePKe84lSivZMVh9ObtNalcEQr +sqxBziNOMJM2mh5V2NdxiK2E1gCZ959wOQ8yzM6gGC+wW8w4zwULhv4JimQDjk+G +kAdiGL7+WAxrNWUulvm8khFt2nOlucJg4IAYVt2SI1AFMt/YSXoA4wMwM9QcHGj0 +1A069IxX93WVhUpIL1Avwz+KJK0BPY6SM8LYUy6V50Hojp76BB7VD6SxQrSoceUo +6cRNDtCmofOlltfeUJLr1mI4S2tM50bQVsHD92EJBA== +-----END CERTIFICATE----- diff --git a/contrib/rcp/usage_example/certs/example_ssl_rcpc_rcps_cert.pem b/contrib/rcp/usage_example/certs/example_ssl_rcpc_rcps_cert.pem new file mode 100644 index 00000000..2c401392 --- /dev/null +++ b/contrib/rcp/usage_example/certs/example_ssl_rcpc_rcps_cert.pem @@ -0,0 +1,115 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 42:38:a5:6f:70:53:40:e4:a4:1a:2c:0f:fc:81:13:42 + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=Easy-RSA CA + Validity + Not Before: Apr 29 12:09:35 2026 GMT + Not After : Aug 1 12:09:35 2028 GMT + Subject: CN=example_ssl_rcpc_rcps_cert + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:ae:0f:e1:ee:fc:f6:db:75:45:c0:f4:49:72:46: + 3d:e3:db:0c:c4:34:d2:9e:49:d4:86:4f:19:0d:55: + 70:50:81:e4:e6:64:56:a8:58:e8:e6:54:0a:16:bc: + f4:4b:84:cd:1d:b9:2e:ed:62:b6:cd:62:35:8b:81: + 18:ab:ff:63:f5:c1:dc:16:3e:a8:dc:ac:11:dd:43: + 12:f8:ef:f2:f1:af:84:fd:83:fe:a8:d3:46:7d:77: + e6:ae:95:61:a6:c9:99:6b:40:61:8d:6e:7e:66:1e: + 97:77:b0:e8:b7:3d:3a:d5:d7:d3:ee:66:95:62:83: + 14:cc:5e:32:ff:9e:bd:f1:06:e6:8d:6a:7c:0a:27: + 22:19:b9:06:09:cf:ef:c7:dc:e8:8f:04:4b:83:0d: + cc:8d:b1:c2:cf:ab:40:25:6e:f2:bf:b7:c6:1d:8f: + d2:fc:3d:c8:a1:be:4a:09:b9:91:e3:76:4f:c7:9b: + fc:2f:de:d9:bb:eb:df:d3:d8:8c:72:79:bd:bf:10: + 8b:01:e6:0f:7f:bb:f6:75:31:5a:40:ad:df:e1:07: + e6:12:12:b2:d3:99:d0:bd:24:5a:9a:ce:62:4f:da: + fe:0d:df:09:ae:da:04:83:54:e8:cb:68:c0:57:78: + c2:f4:68:42:d7:f4:81:4a:a3:b4:4e:0b:49:95:26: + 1d:15 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Subject Key Identifier: + 8E:99:9D:C0:70:98:57:16:08:8E:DF:6E:51:78:A6:86:18:FF:06:52 + X509v3 Authority Key Identifier: + keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB + DirName:/CN=Easy-RSA CA + serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73 + X509v3 Extended Key Usage: + TLS Web Server Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + X509v3 Subject Alternative Name: + DNS:127.0.0.1, IP Address:127.0.0.1 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 3e:56:20:f9:3b:fa:13:6e:7e:a9:80:a6:15:18:01:82:f1:b8: + 4d:1b:f1:ee:da:ed:50:f7:3b:13:01:a5:14:f9:4c:0e:34:57: + dc:e6:d1:7e:02:30:af:3b:fd:c9:ae:18:16:c9:3b:0a:4e:20: + da:cd:e8:cc:05:0c:b3:7d:6f:e5:15:ff:66:59:6b:fe:ff:1a: + ef:ca:b5:3a:1a:ad:dd:f6:19:43:d9:2b:61:18:29:95:b4:0c: + 1e:b2:4a:ce:80:d3:1b:59:dc:62:ec:50:21:37:9c:2f:7a:4d: + c2:ac:de:1b:1d:a3:25:e0:e8:33:42:cf:77:31:2a:f2:44:36: + ef:59:89:da:6c:3e:9a:e8:d7:06:39:17:d5:78:82:6d:b6:63: + 3f:9a:40:3b:e6:12:58:52:3d:63:4e:85:0b:02:cb:40:d2:8a: + 59:8d:8f:ee:4a:c8:97:91:51:a9:2f:1b:15:81:9c:20:dd:94: + 08:6f:ac:fa:c6:28:90:6c:17:5a:23:87:9a:5b:e5:c6:2e:f3: + 09:66:de:76:1b:60:42:c1:5c:71:88:87:f6:7b:cb:e3:7e:14: + 67:c9:a0:15:98:b6:7b:75:40:9a:08:fc:77:39:3a:23:cb:e3: + 78:7d:57:f9:a7:66:36:b4:b5:07:de:61:3a:dd:07:58:b3:4f: + 41:f6:f4:d9 +-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgIQQjilb3BTQOSkGiwP/IETQjANBgkqhkiG9w0BAQsFADAW +MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yNjA0MjkxMjA5MzVaFw0yODA4MDEx +MjA5MzVaMCUxIzAhBgNVBAMMGmV4YW1wbGVfc3NsX3JjcGNfcmNwc19jZXJ0MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArg/h7vz223VFwPRJckY949sM +xDTSnknUhk8ZDVVwUIHk5mRWqFjo5lQKFrz0S4TNHbku7WK2zWI1i4EYq/9j9cHc +Fj6o3KwR3UMS+O/y8a+E/YP+qNNGfXfmrpVhpsmZa0BhjW5+Zh6Xd7Dotz061dfT +7maVYoMUzF4y/5698QbmjWp8CiciGbkGCc/vx9zojwRLgw3MjbHCz6tAJW7yv7fG +HY/S/D3Iob5KCbmR43ZPx5v8L97Zu+vf09iMcnm9vxCLAeYPf7v2dTFaQK3f4Qfm +EhKy05nQvSRams5iT9r+Dd8JrtoEg1Toy2jAV3jC9GhC1/SBSqO0TgtJlSYdFQID +AQABo4G+MIG7MAkGA1UdEwQCMAAwHQYDVR0OBBYEFI6ZncBwmFcWCI7fblF4poYY +/wZSMFEGA1UdIwRKMEiAFLrNjgN3SkkWu9YubKOfS8NMWpWroRqkGDAWMRQwEgYD +VQQDDAtFYXN5LVJTQSBDQYIUEv1f0yjVtkr+RNYLItZ33eTJwHMwEwYDVR0lBAww +CgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMBoGA1UdEQQTMBGCCTEyNy4wLjAuMYcE +fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAPlYg+Tv6E25+qYCmFRgBgvG4TRvx7trt +UPc7EwGlFPlMDjRX3ObRfgIwrzv9ya4YFsk7Ck4g2s3ozAUMs31v5RX/Zllr/v8a +78q1Ohqt3fYZQ9krYRgplbQMHrJKzoDTG1ncYuxQITecL3pNwqzeGx2jJeDoM0LP +dzEq8kQ271mJ2mw+mujXBjkX1XiCbbZjP5pAO+YSWFI9Y06FCwLLQNKKWY2P7krI +l5FRqS8bFYGcIN2UCG+s+sYokGwXWiOHmlvlxi7zCWbedhtgQsFccYiH9nvL434U +Z8mgFZi2e3VAmgj8dzk6I8vjeH1X+admNrS1B95hOt0HWLNPQfb02Q== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuD+Hu/PbbdUXA +9ElyRj3j2wzENNKeSdSGTxkNVXBQgeTmZFaoWOjmVAoWvPRLhM0duS7tYrbNYjWL +gRir/2P1wdwWPqjcrBHdQxL47/Lxr4T9g/6o00Z9d+aulWGmyZlrQGGNbn5mHpd3 +sOi3PTrV19PuZpVigxTMXjL/nr3xBuaNanwKJyIZuQYJz+/H3OiPBEuDDcyNscLP +q0AlbvK/t8Ydj9L8PcihvkoJuZHjdk/Hm/wv3tm769/T2Ixyeb2/EIsB5g9/u/Z1 +MVpArd/hB+YSErLTmdC9JFqazmJP2v4N3wmu2gSDVOjLaMBXeML0aELX9IFKo7RO +C0mVJh0VAgMBAAECggEAGBhmQhdeE+Cu1Ihsn2dWW3PAF2wpiNR3GVWbRfOBHf/x +QCx9K4ZNTU8ua1niZo7edyIiuyVaYWGaQHLRR8QNoiBhN2oapZujSHInzvKmeqr9 +ubt7NgMzQ5ykwB+5OiW3uXda2cGFOV08QgspF+6ftakQMzUbslyrdSQIIscmi5Ya +uTDvE6+lBFinxy3RHFKVCZ3UrsDwfHR4eTUmgCHRB27joB7DFXL32amv0M8HjoGz +EZKGJgTwmRf9U4z4D4wCnOfVAPlsuthKUqMuTlBg0ZEstMZrzlP4suT2ieku0Usv +0XbJ38VozPYYFdR7nApVVvrJgHzI9cpoUbGto4BLOQKBgQDbXjFVLffOec8hv9dN +2VGZQmK61S9OrbvTnEOlxJd+kRid71X1pV5TuPKJJQtUJXf429bQOs40YbLeOmJt +BiRSR5yIBH7hDDC/c0ynqunstwDlgz+QX2Oh2B4alvVaWy0rZYF6NpBiI0+R5r5V +C2fHRS4LLPoflg83+CMubyLS6QKBgQDLIOXxlp1JQTzXhJkrkytLkafmEHAafovt +wbRD50/s+dl16BRX12sK0gXj2vwu0FleUD6Z7afDfspmvQdg3fyDxYw9Q+vw5LYQ +7BvoVU99o1m468yXwX/v36peCt4nOpwkJZKJfjgxjnMJByyeSUgL9uW4K+0D0LBV +a5Iv7QklTQKBgH30BkVPIHKIE/rfyIJlXemuaTu2/fOh4y9sEJdUWluMeeLssaFa +ct+FWJSQFYIaBVl4+E0VBqKi2e2o/ix1E1O+1ExwsF0M/8xdKk024BtPNA+TnWKK +so0Rpq9Dr9pScYvyOzZtr9b5SU2PfAcehlavDPHTwEV0hoZvTdvyab9JAoGADMBJ +7vp3cSvJN/Y470VTyHCiS4zonKEpA4nPWRviJowgnIgvDryVGZ7Jg94xSncFxSfg +ZiVHDLye1Ag1uFz3BwaVoRrsarjQvQs1TUZdsRNaBIO42iXpdBNkTHb+LxQ8zQAW +zM7BlErO6dgrctxCy416Ki+Ht1+YUiRojt2gX1kCgYBqytUy+XkPi5j3Ga29xcvP +WI3Uc8RI2GmoAmrw5QFiSG6lNXAzfo2ZNQbFnxgxeMOG9fV9yzBdIjXWNWr0E/KH +Fsb65R8iIrXQB9BZjuQqjz9nDm7eZZUBNGGbQ4DgSepnp194gXC5DoAElzuwOXbE +pY/kM1KwlpUR3J3LeF3i+Q== +-----END PRIVATE KEY----- diff --git a/contrib/rcp/usage_example/certs/example_ssl_rcpm_rcps_cert.pem b/contrib/rcp/usage_example/certs/example_ssl_rcpm_rcps_cert.pem new file mode 100644 index 00000000..429c09ad --- /dev/null +++ b/contrib/rcp/usage_example/certs/example_ssl_rcpm_rcps_cert.pem @@ -0,0 +1,115 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + e7:09:ab:70:b5:dc:1f:11:d9:2a:23:04:39:87:34:f3 + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=Easy-RSA CA + Validity + Not Before: Apr 29 12:09:35 2026 GMT + Not After : Aug 1 12:09:35 2028 GMT + Subject: CN=example_ssl_rcpm_rcps_cert + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:cc:79:9b:d3:f3:1f:41:9f:00:48:cd:47:0b:ae: + b9:1c:4e:3e:55:e2:4e:5f:a8:cc:13:d5:dd:bd:f0: + 01:4c:19:ae:e3:a9:09:06:89:92:49:f7:bb:90:28: + fb:8c:22:69:b5:f5:a0:50:3d:97:0f:1e:1d:b1:a8: + 57:9b:d7:e2:0d:99:67:7f:02:82:0c:9c:8e:dd:13: + 03:28:93:b5:cb:7e:b5:78:06:10:bf:7b:55:c3:f7: + 10:8b:20:4a:1c:f9:f1:b2:fa:f1:c7:44:9d:0a:ce: + ef:8d:f9:e8:ff:d1:c1:69:ec:8e:5f:11:cc:c9:98: + d5:1c:33:e2:5b:7a:4d:34:dc:76:c3:cd:db:4c:93: + d1:08:78:6f:3c:9a:ee:74:39:1e:cd:65:1e:c9:35: + cc:3b:2b:9e:d7:49:10:8e:58:85:b0:10:5b:90:1e: + f1:5e:d5:92:04:93:f9:33:c6:9d:77:63:d1:33:46: + 5b:98:ff:9a:a8:f5:df:f7:84:21:e2:88:28:7a:a4: + c6:0d:9f:25:7e:0d:73:5b:d5:53:4a:90:79:94:37: + 14:f3:c8:75:76:d4:1c:32:51:bf:58:16:74:d5:8d: + 18:b6:53:f4:ab:cb:91:a8:8c:a3:ca:3c:5c:35:b6: + 5f:62:57:37:5a:75:28:b7:4d:26:aa:ea:50:da:a4: + 1c:55 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Subject Key Identifier: + 47:92:B5:81:8B:5C:14:98:B3:83:B6:EB:06:9F:43:F3:3A:7E:ED:24 + X509v3 Authority Key Identifier: + keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB + DirName:/CN=Easy-RSA CA + serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73 + X509v3 Extended Key Usage: + TLS Web Server Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + X509v3 Subject Alternative Name: + DNS:127.0.0.1, IP Address:127.0.0.1 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 6d:31:e6:29:d2:3b:a8:90:5c:4b:ac:61:15:95:5d:70:66:a5: + 77:9d:88:47:49:73:75:be:70:69:d8:2f:62:82:5e:83:86:3b: + a8:48:3f:f1:5f:22:ae:81:23:64:c4:f2:2b:dd:4d:be:e5:6a: + 26:a5:ea:c7:ba:1b:3e:6a:34:03:5a:f1:49:28:5f:56:4a:a6: + 0e:1b:7a:07:48:76:95:b6:4b:f5:3f:b9:67:2e:e0:33:06:80: + d4:d6:01:a5:76:01:c0:a5:18:e5:38:8b:52:73:6e:6d:45:50: + b7:9a:ab:86:5d:e3:65:b4:b8:c7:ee:b2:dc:bf:e3:d5:bb:e4: + 91:eb:f5:0c:38:22:5e:37:54:9e:ba:96:25:10:04:18:23:f7: + ae:73:4d:d0:aa:03:81:b4:89:36:97:15:da:1a:60:a0:98:5f: + 03:f8:1b:22:83:57:41:4b:12:28:7d:8d:ea:88:74:24:28:5c: + 53:41:89:5e:9a:da:fd:7b:bf:60:dc:de:9b:49:ce:5c:a3:b2: + 01:7d:1d:cb:28:8c:ba:f4:7b:5d:2b:cb:15:5b:2a:97:1a:d1: + f9:e7:12:e3:43:b9:f4:2a:88:dd:6d:b6:a0:72:d3:bd:63:23: + e9:d7:f0:ac:b5:6d:0d:f2:d9:8b:2c:c4:35:5b:4d:83:dc:e8: + 7d:0b:3d:a3 +-----BEGIN CERTIFICATE----- +MIIDhTCCAm2gAwIBAgIRAOcJq3C13B8R2SojBDmHNPMwDQYJKoZIhvcNAQELBQAw +FjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjYwNDI5MTIwOTM1WhcNMjgwODAx +MTIwOTM1WjAlMSMwIQYDVQQDDBpleGFtcGxlX3NzbF9yY3BtX3JjcHNfY2VydDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMx5m9PzH0GfAEjNRwuuuRxO +PlXiTl+ozBPV3b3wAUwZruOpCQaJkkn3u5Ao+4wiabX1oFA9lw8eHbGoV5vX4g2Z +Z38Cggycjt0TAyiTtct+tXgGEL97VcP3EIsgShz58bL68cdEnQrO74356P/RwWns +jl8RzMmY1Rwz4lt6TTTcdsPN20yT0Qh4bzya7nQ5Hs1lHsk1zDsrntdJEI5YhbAQ +W5Ae8V7VkgST+TPGnXdj0TNGW5j/mqj13/eEIeKIKHqkxg2fJX4Nc1vVU0qQeZQ3 +FPPIdXbUHDJRv1gWdNWNGLZT9KvLkaiMo8o8XDW2X2JXN1p1KLdNJqrqUNqkHFUC +AwEAAaOBvjCBuzAJBgNVHRMEAjAAMB0GA1UdDgQWBBRHkrWBi1wUmLODtusGn0Pz +On7tJDBRBgNVHSMESjBIgBS6zY4Dd0pJFrvWLmyjn0vDTFqVq6EapBgwFjEUMBIG +A1UEAwwLRWFzeS1SU0EgQ0GCFBL9X9Mo1bZK/kTWCyLWd93kycBzMBMGA1UdJQQM +MAoGCCsGAQUFBwMBMAsGA1UdDwQEAwIFoDAaBgNVHREEEzARggkxMjcuMC4wLjGH +BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAG0x5inSO6iQXEusYRWVXXBmpXediEdJ +c3W+cGnYL2KCXoOGO6hIP/FfIq6BI2TE8ivdTb7laial6se6Gz5qNANa8UkoX1ZK +pg4begdIdpW2S/U/uWcu4DMGgNTWAaV2AcClGOU4i1Jzbm1FULeaq4Zd42W0uMfu +sty/49W75JHr9Qw4Il43VJ66liUQBBgj965zTdCqA4G0iTaXFdoaYKCYXwP4GyKD +V0FLEih9jeqIdCQoXFNBiV6a2v17v2Dc3ptJzlyjsgF9HcsojLr0e10ryxVbKpca +0fnnEuNDufQqiN1ttqBy071jI+nX8Ky1bQ3y2YssxDVbTYPc6H0LPaM= +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMeZvT8x9BnwBI +zUcLrrkcTj5V4k5fqMwT1d298AFMGa7jqQkGiZJJ97uQKPuMImm19aBQPZcPHh2x +qFeb1+INmWd/AoIMnI7dEwMok7XLfrV4BhC/e1XD9xCLIEoc+fGy+vHHRJ0Kzu+N ++ej/0cFp7I5fEczJmNUcM+Jbek003HbDzdtMk9EIeG88mu50OR7NZR7JNcw7K57X +SRCOWIWwEFuQHvFe1ZIEk/kzxp13Y9EzRluY/5qo9d/3hCHiiCh6pMYNnyV+DXNb +1VNKkHmUNxTzyHV21BwyUb9YFnTVjRi2U/Sry5GojKPKPFw1tl9iVzdadSi3TSaq +6lDapBxVAgMBAAECggEADJFN6K9OWhYX1PcEWUgOLxqdCLd95Iccsfxot7ekcMUP +A4WnHRyACLqor9c2V3o2//IpU2fnB2IXu6ISmRd3WKl3hm4vnZmoIJeTpQm9Iv/g ++fqkyrbIgktcHDJUySal+n+jiYFNW2B1h1xXUT/scMz+FthNJg1Azfi0vorMFjCk +SBOSo7BQ2hiQ83FneVJU1TsxD4S4IBLx9fF6AW05norRmvm17Ip2pKk4QYzKiOI3 +NIoSwbOgU0Vp1X3MMlilM5ZZdN3a9lI6lfBZE682WOxBmH67mKMQnR8aC+nwynQ+ +45pUQIn8Fjx2hQHehbD7ZNw9Nob9AyGWqWGKFV74IQKBgQDox/j7dCHNm1mz1QRm +Q2YyN4OGXJI97t9Gd8UdNCv5bAkdx2cR2lmP3tRc6iAzrfNIpPCGaXo1vwJx477X +wO95W2b4hfm3j6v0cqRbsFzzVIHZUB77pXfAJfZeICpoqu0vxn5nb+yPgzgmToLX +pbIDdqWafzzrDLTmLCfwfKDI9QKBgQDg3tgpAXo8WCL4phNuW44XQ172lPTijpn+ +wj1Z0rBrS806gL/+QvZZLS1WCym/QBV7TgGlxIJbmAfghcGyin3NliskSHAiccxG +/9eCSQes8czfsVj6qmBMwyff5r+wmk662qV0u07UHmuykYk3Dgs/zYdwq2SsTlL4 +Y9eRjutp4QKBgQCONg0wYcR8/hmROeRULXzz1OJvZYKaf6K8RFOSAduTp6LyJG4d +hA4PTQzkLsy5hd4JVWr0UuAskaMGvSJMYTxsIaEI16C1ufpNfvRWZ6qBpfEmOEKV +boN4Sjj3TCNcioAZHeT/gGs/SeU10eUxpbLZVtTZTD6FQuAJdpR34UvBOQKBgFNM +mXxPLM2vxHyhYK9PwQoDDel/8lr+gjMqFvnwHyQP911FllmEyqbsIlAuYG+VOJ/t +nJSgf72YSsq0IbWWsdV3XFHbd5Z62zYtzdJYZTx+cesnUhPBC11EKcA6RSYRczqq +hgIA5MmU30ZNvSukyyv+Yb6t7uQZO4kByzgDXldhAoGBALgRkAHxgbKUXp5XyCDJ +e8dwVx0g9tfDM/DEZtU/Si5oUaunBaPV/Byov7OXOT02V8JLnA5ChUYgwUFI030x +QL/3eK12Qh5Gb9VabvYCicDRk4GzmqZU9Wcvm1zgbUr5jY8Lou44nFjol/Y1m70n +51WZbVkkWmBZO5m3NqN66SkJ +-----END PRIVATE KEY----- diff --git a/contrib/rcp/usage_example/certs/example_ssl_rcps_rcpm_cert.pem b/contrib/rcp/usage_example/certs/example_ssl_rcps_rcpm_cert.pem new file mode 100644 index 00000000..5adc3c17 --- /dev/null +++ b/contrib/rcp/usage_example/certs/example_ssl_rcps_rcpm_cert.pem @@ -0,0 +1,115 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 28:96:2e:a1:40:e0:7e:f1:fb:63:1a:f4:53:6f:ce:fb + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=Easy-RSA CA + Validity + Not Before: Apr 29 12:09:35 2026 GMT + Not After : Aug 1 12:09:35 2028 GMT + Subject: CN=example_ssl_rcps_rcpm_cert + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:ae:11:46:ef:d1:81:34:dd:23:5d:54:40:f3:9c: + 85:35:95:a6:91:57:92:5c:bf:eb:40:34:69:eb:db: + c0:86:3c:7b:ff:9c:d7:ba:0e:41:57:84:15:cd:94: + f1:48:63:50:9c:34:97:ee:be:be:b0:27:d8:fd:cd: + 8a:cf:85:ff:08:1f:07:d8:28:96:0e:e4:2d:d0:8b: + df:a8:fa:41:47:a0:a2:80:2e:2e:58:01:cc:6f:43: + 5c:c2:fb:84:a7:ff:9e:97:bb:b3:a3:1f:63:64:73: + 8d:73:dd:f4:7e:96:d7:6b:b3:cb:e2:35:59:55:e0: + e7:e3:c0:41:f8:b6:0f:c5:46:4c:cd:0e:91:80:ef: + e3:43:f0:72:26:12:10:be:83:a2:db:23:2d:b4:b1: + 07:5a:b1:b3:10:9c:09:69:98:42:79:81:77:5e:22: + e4:71:47:70:27:15:2c:a7:13:c2:6d:44:59:b4:73: + c9:bb:27:7f:d6:e8:3d:85:bb:36:f6:cb:71:36:11: + b1:99:1a:1d:1a:15:dd:cd:65:7f:cd:cc:10:00:49: + ed:07:2d:7b:15:88:be:73:ba:1d:15:69:bc:d3:02: + 55:ea:dc:2c:3f:0b:cd:18:57:59:7a:e3:09:b2:89: + cd:d6:e7:f6:95:c4:2e:8a:53:2b:a8:96:82:94:53: + 00:77 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Subject Key Identifier: + 60:BD:48:06:68:15:D4:DC:ED:EE:E4:C7:B1:9F:C4:93:6D:50:3A:77 + X509v3 Authority Key Identifier: + keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB + DirName:/CN=Easy-RSA CA + serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73 + X509v3 Extended Key Usage: + TLS Web Server Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + X509v3 Subject Alternative Name: + DNS:127.0.0.1, IP Address:127.0.0.1 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + c5:35:61:58:23:e2:69:da:6c:d5:41:ab:a8:70:f4:dd:cc:a0: + a3:3d:84:89:93:b6:7f:69:7d:10:35:9d:c5:d1:0d:db:d2:d7: + 36:af:d4:54:30:14:a7:5d:31:ca:5c:13:92:d5:60:50:f8:56: + 4a:cb:16:b1:b3:b1:03:bf:96:53:77:1f:4a:0f:9c:29:2b:bf: + a4:e0:da:6f:ad:13:c7:2d:8e:18:c4:72:50:17:ed:1f:36:51: + 7a:12:9f:fc:a6:d6:c8:55:e0:db:ea:16:d6:22:0d:a2:cb:eb: + b2:ba:07:92:2f:db:33:d6:a2:0c:ec:89:29:f1:96:40:e5:0b: + e6:1f:08:50:d6:29:87:a8:20:b2:e2:17:50:25:ff:53:36:ee: + 7f:ce:e6:1d:ed:b3:16:61:18:42:a9:17:9e:a6:86:0d:a5:fc: + f9:42:c8:50:48:74:72:35:eb:8c:ff:4d:e8:98:88:a0:b4:b3: + d0:82:b3:2f:ea:19:d7:d5:ac:47:35:96:24:37:34:0c:7a:a2: + e0:4d:99:a7:55:61:85:1e:7e:6a:23:77:f5:07:13:e6:50:5c: + 65:00:13:f6:b5:4b:5b:8c:11:c3:5d:af:ba:41:e9:84:1d:f1: + a4:70:16:28:c2:be:6e:d8:67:38:c5:a0:ba:8a:64:6f:27:ce: + 63:a0:92:9b +-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgIQKJYuoUDgfvH7Yxr0U2/O+zANBgkqhkiG9w0BAQsFADAW +MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yNjA0MjkxMjA5MzVaFw0yODA4MDEx +MjA5MzVaMCUxIzAhBgNVBAMMGmV4YW1wbGVfc3NsX3JjcHNfcmNwbV9jZXJ0MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhFG79GBNN0jXVRA85yFNZWm +kVeSXL/rQDRp69vAhjx7/5zXug5BV4QVzZTxSGNQnDSX7r6+sCfY/c2Kz4X/CB8H +2CiWDuQt0IvfqPpBR6CigC4uWAHMb0NcwvuEp/+el7uzox9jZHONc930fpbXa7PL +4jVZVeDn48BB+LYPxUZMzQ6RgO/jQ/ByJhIQvoOi2yMttLEHWrGzEJwJaZhCeYF3 +XiLkcUdwJxUspxPCbURZtHPJuyd/1ug9hbs29stxNhGxmRodGhXdzWV/zcwQAEnt +By17FYi+c7odFWm80wJV6twsPwvNGFdZeuMJsonN1uf2lcQuilMrqJaClFMAdwID +AQABo4G+MIG7MAkGA1UdEwQCMAAwHQYDVR0OBBYEFGC9SAZoFdTc7e7kx7GfxJNt +UDp3MFEGA1UdIwRKMEiAFLrNjgN3SkkWu9YubKOfS8NMWpWroRqkGDAWMRQwEgYD +VQQDDAtFYXN5LVJTQSBDQYIUEv1f0yjVtkr+RNYLItZ33eTJwHMwEwYDVR0lBAww +CgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMBoGA1UdEQQTMBGCCTEyNy4wLjAuMYcE +fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAxTVhWCPiadps1UGrqHD03cygoz2EiZO2 +f2l9EDWdxdEN29LXNq/UVDAUp10xylwTktVgUPhWSssWsbOxA7+WU3cfSg+cKSu/ +pODab60Txy2OGMRyUBftHzZRehKf/KbWyFXg2+oW1iINosvrsroHki/bM9aiDOyJ +KfGWQOUL5h8IUNYph6ggsuIXUCX/Uzbuf87mHe2zFmEYQqkXnqaGDaX8+ULIUEh0 +cjXrjP9N6JiIoLSz0IKzL+oZ19WsRzWWJDc0DHqi4E2Zp1VhhR5+aiN39QcT5lBc +ZQAT9rVLW4wRw12vukHphB3xpHAWKMK+bthnOMWguopkbyfOY6CSmw== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuEUbv0YE03SNd +VEDznIU1laaRV5Jcv+tANGnr28CGPHv/nNe6DkFXhBXNlPFIY1CcNJfuvr6wJ9j9 +zYrPhf8IHwfYKJYO5C3Qi9+o+kFHoKKALi5YAcxvQ1zC+4Sn/56Xu7OjH2Nkc41z +3fR+ltdrs8viNVlV4OfjwEH4tg/FRkzNDpGA7+ND8HImEhC+g6LbIy20sQdasbMQ +nAlpmEJ5gXdeIuRxR3AnFSynE8JtRFm0c8m7J3/W6D2Fuzb2y3E2EbGZGh0aFd3N +ZX/NzBAASe0HLXsViL5zuh0VabzTAlXq3Cw/C80YV1l64wmyic3W5/aVxC6KUyuo +loKUUwB3AgMBAAECggEABGfLiSZJmYeUmDgZrLtkDlx16sJx9zGkR+u2V+cn6D3U ++uiCoo2EedfjSrYKT/AI35Xf19sGrc6ptJgPJfwY3aEWFxJv5HtB7ZVHWS98QiPT ++QqHgb1fPzGlQgoQ7Bo8GVBW1joPz0Bdbsv0ntTn1CyowauNUe8Z71mzpxJ0iQ7u +Z8LNoD7INEAZDBjHVov6pLGDHS9KFxGy20WG549mE37I4QxyxetJEgmuyhJkZOQ4 +noEWiCMQjGsSg4YuSc1GS1jAVf3p2g3/TiheD/31r1jleY/T5s2qYC6MJ4vY+7yA +5sl7m8A47i0lHSKBdR3nWz+vXEsxB0nXK3Sulyt9AQKBgQDYRZ1nEe32CCZcWn/7 +nG+9e6XNOe9E25skuvY/uEpikt92kdnyZOgHFfwM9Nv+w2IWGLZ8MmDDJ6/4hGvj +fJjcOUb3d/SJiivPvdRC9GYMrDUZe5AJ3p6fPqi4IeZOw7bMTcVB6aHAr9KoO//J +2t0WNvwzkOyl6KlhaQEIDK8bOQKBgQDOCvWvUEg8nHaKcmN0uQaKuPvCorhnyqUT +VFqLlSrYC79ffHCwp2y8nkFUnxpeHXENrtHtcrdnKBNkgGEn2l4xs63CjhKrBuIG +bDjrtp3vKlHRhQjX6HkrEEuUk52wYzuX7CfU8nTZnrtV74MocOEewJVW+84Rtwiw +vIUcgfgJLwKBgAbgj9TLOSntsGqXZiJ2Iwd/exI/mWAzK4fLejEkhxkDWp/Gm4ud +sdMn28/9qVE8nU3ek073uyP5ixr3+wZM2/+EwsDzy47kGeiNPMa0Rtp4T2f0CeyG +a7zcnTjduxkeGB3/CxrBdydNcAFxhvzAPO+L6BErtpq//0Ldt+6tmJPhAoGAFxEh +Cjx5qdd2ae9+dO3V7qfg/5xJ+sy0CGL0NBZCEqfWB/GdiBlmUgOBmuCpCgpPwtFk +jSm/oJva9/BrcBPBYd0Uweg37M+7dC6ffLwYGFNrj4JOSCWtkwWjAII6MCob3NlC +aFOwg0CDBo7m5xskCNZUocVU/6S3I1onqNZgF18CgYAczBf1NS4VPZvebrutTBEH +zyUX3XU9mR+dy0ncCGNVS8zYMtz7cweZInzNB2cTOfisIHzzdOnazm7D9uzPsREi +pKgaL+ErWYDlGiDTxMtGSRPTWGocYBYdU6y/0bobhZb0qyvyRhpGvPK0ReMUuvqu +FkNgoQ1lo0n6vawvxWW8Mw== +-----END PRIVATE KEY----- diff --git a/contrib/rcp/usage_example/certs/make_certs.sh b/contrib/rcp/usage_example/certs/make_certs.sh new file mode 100755 index 00000000..a3d91644 --- /dev/null +++ b/contrib/rcp/usage_example/certs/make_certs.sh @@ -0,0 +1,49 @@ +#!/bin/bash +EASYRSA=/usr/share/easy-rsa/easyrsa +CA_NAME="example_ssl_rcp_ca_cert" + +export EASYRSA_PASSIN=pass:test +export EASYRSA_PASSOUT=pass:test + +echo "Cleaning up..." +rm -rf ./ca +rm -rf ./*.pem +rm -rf ./*.key +rm -rf ./*.crt + +echo "Creating CA cert..." +mkdir -p ./ca +cd ./ca +$EASYRSA init-pki +cp ../vars ./pki/ +$EASYRSA --batch build-ca +cp ./pki/ca.crt ../$CA_NAME.crt + +echo "Creating server certs..." +# Secures connection between RCP-Client and RCP-Server: +$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcpc_rcps_cert nopass + +# Secures connection between RCP-Module and RCP-Server (module description): +$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcpm_rcps_cert nopass + +# Secures connection between RCP-Server and RCP-Module (command execution): +$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcps_rcpm_cert nopass + +echo "Collecting server certs..." +cp ./pki/issued/* ../ +cp ./pki/private/* ../ +cd .. +rm ./ca.key + +echo "Merging server certs..." +for CRT in ./*.crt; do + CRT_NAME=`basename ${CRT%.*}` + if [ -f $CRT_NAME.key ]; then + cat $CRT_NAME.crt $CRT_NAME.key > $CRT_NAME.pem + rm $CRT_NAME.key + rm $CRT_NAME.crt + fi +done + +echo "Finalizing..." +rm -rf ./ca diff --git a/contrib/rcp/usage_example/encrypt_card_data.sh b/contrib/rcp/usage_example/encrypt_card_data.sh new file mode 100755 index 00000000..4d27f947 --- /dev/null +++ b/contrib/rcp/usage_example/encrypt_card_data.sh @@ -0,0 +1,8 @@ +#!/bin/bash +. ./params.cfg + +PYTHONPATH=$PYSIM_DIR ../../csv-encrypt-columns.py \ + --csv-column-key kic:$CSV_COLUMN_KEY \ + --csv-column-key kid:$CSV_COLUMN_KEY \ + --csv-column-key kik:$CSV_COLUMN_KEY \ + card_data.csv diff --git a/contrib/rcp/usage_example/params.cfg b/contrib/rcp/usage_example/params.cfg new file mode 100644 index 00000000..b33e25d8 --- /dev/null +++ b/contrib/rcp/usage_example/params.cfg @@ -0,0 +1,40 @@ +# 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" + +# CSV column key to decrypt KIC, KID and KIK in csv_data.csv.encr +# (use encrypt_card_data.sh to regenerate csv_data.csv.encr from csv_data.csv) +CSV_COLUMN_KEY="00112233445566778899AABBCCDDEEFF" + +# PCSC reader that the RCP Client shall use +PCSC_READER=0 + +# 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..a02b9a02 --- /dev/null +++ b/contrib/rcp/usage_example/rcp_module.py @@ -0,0 +1,126 @@ +#!/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 +from pySim.global_platform import GpCardKeyset, SCP02, establish_scp, release_scp, install, store_data +from Cryptodome.Random import get_random_bytes +from osmocom.utils import h2b, b2h + +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'}, + } + ]}, + {'name' : 'unlock_aram', + 'help': 'unlock a locked ARA-M applet on a sysmoISIM-SJA5', + 'args' : [], + 'get_keys' : {'uicc' : ['KIC', 'KID', 'KIK']}} + ] + suitable_for = [{'atr' : '3b9f96801f878031e073fe211b674a357530350265f8'}] + + retrieve_uicc_keys = ['KIC', 'KID', 'KIK'] + + def cmd_reset(self, hdlr: RcpmCmdSrvConnHdlr) -> int: + hdlr.print("resetting UICC/eUICC ...") + hdlr.card._scc.reset_card() + hdlr.print("ATR is: %s" % hdlr.card._scc.get_atr()) + return 0 + + def cmd_read_binary(self, hdlr: RcpmCmdSrvConnHdlr) -> int: + fid = hdlr.cmd_args.fid + hdlr.print("reading transparent file: %s ..." % fid) + (res, _) = hdlr.card._scc.read_binary(fid) + hdlr.print("file content is: %s" % res) + return 0 + + def cmd_read_record(self, hdlr: RcpmCmdSrvConnHdlr) -> int: + fid = hdlr.cmd_args.fid + record = hdlr.cmd_args.record + hdlr.print("reading linear-fixed file: %s ..." % fid) + (res, _) = hdlr.card._scc.read_record(fid, record) + hdlr.print("file content is: %s" % res) + return 0 + + def cmd_unlock_aram(self, hdlr: RcpmCmdSrvConnHdlr) -> int: + # Select ADF.ISD + hdlr.print("Selecting ADF.ISD ...") + hdlr.lchan.scc.send_apdu_checksw("00a4040408a00000000300000000") + + # Estabish secure channel + hdlr.print("Establishing secure channel ...") + key_ver = 112 + key_enc = hdlr.keys_uicc['KIC'] + key_mac = hdlr.keys_uicc['KID'] + key_dek = hdlr.keys_uicc['KIK'] + security_level = 3 + host_challenge_len = 8 + host_challenge = get_random_bytes(host_challenge_len) + kset = GpCardKeyset(key_ver, h2b(key_enc), h2b(key_mac), h2b(key_dek)) + scp = SCP02(card_keys=kset) + establish_scp(hdlr.lchan, scp, host_challenge, security_level) + + # To prove that it works, we need to do something that actually requires to be authenticated + # via a secure channel. In this example we will send an unlock command to the ARA-M applet + # found on any sysmoISIM-SJA5 card. (see also: https://gitea.osmocom.org/sim-card/aram-applet) + hdlr.print("Unlocking ARA-M applet ...") + ara_m_aid = "a00000015141434c00" + install(hdlr.lchan, 0x20, 0x00, "0000%02x%s000000" % (len(ara_m_aid) // 2, ara_m_aid)) + store_data(hdlr.lchan, h2b("A2"), structure = 'ber_tlv') + + # Release the secure channel + hdlr.print("Done, releasing secure channel ...") + release_scp(hdlr.lchan) + 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..4bef46df --- /dev/null +++ b/contrib/rcp/usage_example/readme.txt @@ -0,0 +1,16 @@ +How to try: + +Go to the directory that contains the usage example: +cd pysim/contrib/rcp/usage_example + +Edit card_data.csv to fill in the SCP02 keys for the ISD of your sysmoISIM-SJA5 + +Start the RCP Server: +./start_rcp_server.sh + +Start the RCP Module: + ./start_rcp_module.sh + +Run the exmple scripts: +./run_rcp_client.sh +(it is also possible to call the run_rcp_client_*.sh scripts individually) diff --git a/contrib/rcp/usage_example/run_rcp_client.sh b/contrib/rcp/usage_example/run_rcp_client.sh new file mode 100755 index 00000000..4d32be24 --- /dev/null +++ b/contrib/rcp/usage_example/run_rcp_client.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +echo "basic help" +echo "====================================================================================" +./run_rcp_client_help.sh +echo "====================================================================================" +echo "" +echo "" + +echo "help for which commands are available" +echo "====================================================================================" +./run_rcp_client_help_cmd.sh +echo "====================================================================================" +echo "" +echo "" + +echo "help for specific commands" +echo "====================================================================================" +./run_rcp_client_help_cmd_specific.sh +echo "====================================================================================" +echo "" +echo "" + +echo "run specific RCP commands" +echo "====================================================================================" +./run_rcp_client_cmd.sh +echo "====================================================================================" +echo "" +echo "" diff --git a/contrib/rcp/usage_example/run_rcp_client_cmd.sh b/contrib/rcp/usage_example/run_rcp_client_cmd.sh new file mode 100755 index 00000000..44b58d6f --- /dev/null +++ b/contrib/rcp/usage_example/run_rcp_client_cmd.sh @@ -0,0 +1,28 @@ +#!/bin/bash +. ./params.cfg + +set -x + +PYTHONPATH=$PYSIM_DIR ../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 + +PYTHONPATH=$PYSIM_DIR ../rcp_client.py $VERBOSE \ + --uri $RCPC_SERVER_URI \ + --ca-cert $CA_CERT \ + -p $PCSC_READER \ + rcp_module_unlock_aram diff --git a/contrib/rcp/usage_example/run_rcp_client_help.sh b/contrib/rcp/usage_example/run_rcp_client_help.sh new file mode 100755 index 00000000..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..18d8b36f --- /dev/null +++ b/contrib/rcp/usage_example/run_rcp_client_help_cmd_specific.sh @@ -0,0 +1,28 @@ +#!/bin/bash +. ./params.cfg + +set -x + +PYTHONPATH=$PYSIM_DIR ../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 + +PYTHONPATH=$PYSIM_DIR ../rcp_client.py $VERBOSE \ + --uri $RCPC_SERVER_URI \ + --ca-cert $CA_CERT \ + -p $PCSC_READER \ + rcp_module_unlock_aram --help diff --git a/contrib/rcp/usage_example/start_rcp_module.sh b/contrib/rcp/usage_example/start_rcp_module.sh new file mode 100755 index 00000000..bd82249e --- /dev/null +++ b/contrib/rcp/usage_example/start_rcp_module.sh @@ -0,0 +1,14 @@ +#!/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 \ + --column-key kic:$CSV_COLUMN_KEY \ + --column-key kid:$CSV_COLUMN_KEY \ + --column-key kik:$CSV_COLUMN_KEY + diff --git a/contrib/rcp/usage_example/start_rcp_server.sh b/contrib/rcp/usage_example/start_rcp_server.sh new file mode 100755 index 00000000..3db000fb --- /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 \ + --csv ./card_data.csv.encr