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
This commit is contained in:
Philipp Maier
2026-04-09 14:09:52 +02:00
parent e92a4ee382
commit efe6e32120
31 changed files with 3610 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env python3
# (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import websockets
import asyncio
import argparse
import logging
from copy import deepcopy
from pathlib import Path
from pySim.log import PySimLogger
from rcp_utils import CltConnHdlr, backtrace, pytype_to_type, load_ca_cert, load_json_schema, JsonValidator
from pySim.transport import init_reader, argparse_add_reader_args, LinkBase
from packaging.version import Version
SERVER_TIMEOUT = 10
# The RCP Client software version shall be incremented when there are changes to the RCP Client (this module) or changes
# to other related modules, which affect the RCP Client. The RCP Client software version is also disclosed towards the
# RCP Server.
RCPC_VERSION_SOFTWARE = "1.0.0"
# The RCP Client protocol version refers to the protocol spoken between RCP Client and RCP Server. The protocol version
# shall be incremented when there are changes to the protocol (JSON Schema and/or application logic, see also
# RCPC_VERSION_PROTOCOL in rcp_server.py).
RCPC_VERSION_PROTOCOL = "1.0.0"
log = PySimLogger.get(Path(__file__).stem)
option_parser = argparse.ArgumentParser(description='RCP Client',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
argparse_add_reader_args(option_parser)
option_parser.add_argument("--verbose", help="Enable verbose logging",
action='store_true', default=False)
option_parser.add_argument("--uri", help="URI of the RCP-Server")
option_parser.add_argument("--ca-cert", help="SSL/TLS CA-Certificate of the RCP-Server")
class RcpcCltConnHdlr(CltConnHdlr):
def __init__(self, sl, *args, **kwargs):
self.sl = sl
super().__init__(*args, **kwargs)
async def check_version(self):
"""
Send the Protocol and Software version of this RCP Client to the RCP Server. The RCP Server will then check
if this client is (still) compatible. If an incompatibility is detected, the connection will be closed.
"""
log.info("Checking version ...")
tx_json = {'rcpc_version': {'software' : RCPC_VERSION_SOFTWARE,
'protocol' : RCPC_VERSION_PROTOCOL}}
log.info("RCP Client version: software=%s, protocol=%s",
RCPC_VERSION_SOFTWARE, RCPC_VERSION_PROTOCOL)
rx_json = await self._transact(tx_json)
rcps_version_software = Version(rx_json['rcpc_version']['software'])
rcps_version_protocol = Version(rx_json['rcpc_version']['protocol'])
rcps_version_info = str(rx_json['rcpc_version'].get('info'))
if rcps_version_info:
log.info("RCP Server version: software=%s, protocol=%s",
rcps_version_software, rcps_version_protocol)
else:
log.info("RCP Server version: software=%s, protocol=%s, %s",
rcps_version_software, rcps_version_protocol, rcps_version_info)
async def describe(self, suitable_for:dict) -> list:
log.info("Requesting module descriptions from RCP Server ...")
tx_json = {'rcpc_hello': {'suitable_for' : suitable_for}}
rx_json = await self._transact(tx_json)
module_descr = rx_json['rcpc_welcome']['module_descr']
if not module_descr:
raise ValueError("No RCP module available for this card")
return module_descr
async def run(self, cmd:str, cmd_argv) -> int:
log.info("Executing command with RCP Server ...")
tx_json = {'rcpc_command': {'cmd' : cmd, 'cmd_argv' : cmd_argv}}
while(True):
rx_json = await self._transact(tx_json)
tx_json = None
if 'rcpc_instr' in rx_json:
rcpc_instr = rx_json['rcpc_instr']
if 'c_apdu' in rcpc_instr:
c_apdu = rx_json['rcpc_instr']['c_apdu']
data, sw = sl.send_apdu(c_apdu)
tx_json = {'rcpc_result': {'r_apdu' : {'data': data.upper(), 'sw': sw.upper()}}}
elif 'reset' in rcpc_instr:
sl.reset_card()
atr = sl.get_atr()
tx_json = {'rcpc_result': {'atr' : atr.upper()}}
elif 'print' in rcpc_instr:
log.info(str(self) + " -- %s", rx_json['rcpc_instr']['print'])
tx_json = {'rcpc_result': {'empty' : None}}
elif 'rcpc_goodbye' in rx_json:
rc = rx_json['rcpc_goodbye']
log.info("Command execution done, rc: %d", rc)
return rc
def check_if_user_needs_basic_help(argv):
"""
The '--uri' argument is the minimum requirement to connect to the RCP Server to retrieve the information about the
dynamic commandline arguments. In case this argument is missing while '--help' or '-h' arguments are present. Then
we will fall back to display only a basic help that contains only the static commandline arguments (see above).
"""
if '--help' in argv or '-h' in argv:
if '--uri' not in argv:
option_parser.parse_args()
sys.exit(1)
def parse_known_arguemnts(argv):
"""
Parse the commandline arguments we know so far. Ignore unknown arguments and filter out '--help' and '-h'
arguments, in case those are present.
"""
argv_filtered = deepcopy(argv)
if '--help' in argv_filtered:
argv_filtered.remove('--help')
if '-h' in argv_filtered:
argv_filtered.remove('-h')
opts, unknown = option_parser.parse_known_args(argv_filtered)
return opts
async def run_rcp_session(opts, sl, ssl_context) -> int:
"""
Connect to the RCP Server, retrieve the module description, use the module description to complete the commandline
argument parser, execute the command that the user has selected.
"""
# Request ATR from card
card_atr = sl.get_atr().upper()
log.info("Detected Card with ATR: %s" % card_atr)
# Connect to RCP server
log.info("RCP Server URI: %s" % opts.uri)
async with websockets.connect(opts.uri, ssl=ssl_context) as websocket:
rcpc_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpc_to_rcps_schema.json"))
rcps_to_rcpc_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpc_schema.json"))
json_validator = JsonValidator(rcps_to_rcpc_schema, rcpc_to_rcps_schema)
client = RcpcCltConnHdlr(sl, websocket, SERVER_TIMEOUT, json_validator)
# Check software and protocol version
await client.check_version()
# Retrieve module description
module_descrs = await client.describe({"atr" : card_atr})
# Complete the commandline parser and set up a dict that we can use as filter
# TODO: Maybe it makes sense to integrate this as a method into the RcpcCltConnHdlr class?
option_subparsers = option_parser.add_subparsers(dest='command', help="RCP command to use", required=True)
sys_argv_filter = {}
for module_descr in module_descrs:
cmd_descr = module_descr['cmd_descr']
for cmd in cmd_descr:
command_name = module_descr['name'] + "_" + cmd['name']
option_parser_cmd = option_subparsers.add_parser(command_name, help=cmd['help'])
sys_argv_filter[command_name] = []
for arg in cmd['args']:
arg['spec'] = pytype_to_type(arg['spec'])
option_parser_cmd.add_argument(arg['name'], **arg['spec'])
sys_argv_filter[command_name].append(arg['name'])
# Re-Parse commandline options with the completed commandline parser. In case commandline help is
# requested. The program is able to display the full help screen and exists.
opts = option_parser.parse_args()
# Filter the relevant command arguments from sys.argv
cmd_argv = []
next_is_value=False
for arg in sys.argv:
if arg in sys_argv_filter[opts.command]:
cmd_argv.append(arg)
next_is_value=True
elif next_is_value is True:
next_is_value=False
cmd_argv.append(arg)
# Run the command and close the connection
rc = await client.run(opts.command, cmd_argv)
await client.close()
return rc
if __name__ == '__main__':
# Setup logging
PySimLogger.setup(print, {logging.WARN: "\033[33m", logging.DEBUG: "\033[90m"}, '--verbose' in sys.argv)
# Since parts of the commandline arguments are retrieved dynamically, we have to resolve a chicken-egg-problem.
# We cannot call option_parser.parse_args() at the beginning, since we haven't received all information to
# complete the option_parser yet. However in order to retrieve the arguments correctly we need to get the
# URI and the parameters for the smartcard reader before we make the connection. The situation is even further
# complicated in case the user requests commandline help.
# To resolve the problem we first check if the user needs basic help (no '--uri' parameter present). If this is the
# case, the program will exit with a basic help screen.
check_if_user_needs_basic_help(sys.argv)
# In all other cases we parse the arguments we know so far. In case the user requests commandline help, we will
# ignore this request and continue. The full help is then displayed later when the option_parser is completed
# afer we have requested the commandline argument descriptions from the RCP Server. (see below)
opts = parse_known_arguemnts(sys.argv)
# Load SSL/TLS CA certificate from file
if opts.ca_cert:
ssl_context = load_ca_cert("RCP Server CA", opts.ca_cert)
else:
ssl_context = None
# Initialize card reader
try:
sl = init_reader(opts)
sl.connect()
except Exception as e:
backtrace("Card reader initialization")
sys.exit(1)
# Run the RCP session
try:
rc = asyncio.run(run_rcp_session(opts, sl, ssl_context))
sys.exit(rc)
except SystemExit as rc:
sys.exit(rc)
except:
backtrace("RCP session")
sys.exit(1)
+424
View File
@@ -0,0 +1,424 @@
#!/usr/bin/env python3
# (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import os
import argparse
import logging
import threading
import asyncio
import websockets
from argparse import Namespace
from copy import deepcopy
from pathlib import Path
from typing import Optional
from osmocom.utils import Hexstr, is_hexstr
from pySim.utils import ResTuple
from pySim.transport import LinkBase
from pySim.commands import SimCardCommands
from pySim.log import PySimLogger
from rcp_utils import SrvSyncConnHdlr, CltConnHdlr, backtrace, pytype_to_type, load_server_cert, load_ca_cert
from rcp_utils import dict_from_key_value_pairs, load_json_schema, JsonValidator
from rcp_server import RCPM_VERSION_PROTOCOL
from websockets.sync.server import serve, ServerConnection
from pySim.app import init_card
from pySim.runtime import RuntimeState
from pySim.cards import CardBase
from pySim.card_key_provider import CardKeyFieldCryptor
from packaging.version import Version
# Response timeout towards the RCP Server (includes RCP Client latency)
RCP_SERVER_TIMEOUT = 30 # sec.
log = PySimLogger.get(Path(__file__).stem)
class RcpsSimLink(LinkBase):
"""
pySim: Transport Link for RCPM (Remote Card Procedure Module)
This is a 'headless' transport link implementation that can only be used from an RCPM module. It merely serves as
an adapter between the pySim transport API and the RCPM command server connection handler.
"""
name = 'RCPM'
def __init__(self, conn_hdlr: SrvSyncConnHdlr, **kwargs):
self.conn_hdlr = conn_hdlr
self._atr = None
super().__init__(**kwargs)
def __str__(self) -> str:
return "rcpm:" + str(self.conn_hdlr)
def _send_apdu(self, apdu: Hexstr) -> ResTuple:
tx_json = {'rcps_instr': {'c_apdu' : apdu.upper()}}
rx_json = self.conn_hdlr._transact(tx_json)
data = rx_json['rcps_result']['r_apdu']['data']
sw = rx_json['rcps_result']['r_apdu']['sw']
return data, sw
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
# In this setting, we do not have/cannot to wait for a card since we are not the entity that handles the
# direct connection to the card. When the procedure begins, we assume that the remote end already has set up
# a connection to the card and made it ready to perform operations on it.
pass
def connect(self):
# In this setting, we do not have/cannot to connect because we are not the entity that handles the direct
# connection to the card. The connection is established by the remote end.
pass
def get_atr(self) -> Hexstr:
return self._atr
def disconnect(self):
# In this setting, we do not have/cannot disconnect because we are not the entity that handles the direct
# connection to the card. The disconnect is eventually done by the remote end when the procedure has finished.
pass
def _reset_card(self):
tx_json = {'rcps_instr': {'reset' : None}}
rx_json = self.conn_hdlr._transact(tx_json)
self._atr = rx_json['rcps_result']['atr']
return 1
class RcpsCltConnHdlr(CltConnHdlr):
"""
The RCP Server client handler is used to connect to the RCP Server when RCP Module is started. The connection is
kept alive until the RCP Module is terminated. This connection is used to exchange management data with the RCP
Server.
"""
def __init__(self, cmd_srv_addr: str, cmd_srv_port: int, module, *args, **kwargs):
self.cmd_srv_addr = cmd_srv_addr
self.cmd_srv_port = cmd_srv_port
self.module = module
super().__init__(*args, **kwargs)
async def check_version(self):
"""
Send the Protocol and Software version of this RCP Module to the RCP Server. The RCP Server and the RCP Module
must always use the same protrocol version.
"""
tx_json = {'rcpm_version': {'protocol' : RCPM_VERSION_PROTOCOL}}
rx_json = await self._transact(tx_json)
rcpm_version_protocol = Version(rx_json['rcpm_version']['protocol'])
if Version(RCPM_VERSION_PROTOCOL) != rcpm_version_protocol:
raise ValueError("Incompatible protocol version %s != %s", Version(RCPM_VERSION_PROTOCOL), rcpm_version_protocol)
async def describe(self):
"""
Send a detailed description about this RCP Module to the RCP Server. This is also the initial message that
the RCP Server expects when an RCP Module connects.
"""
# The rules (dict) in suitable_for (array of dict) may contain hexstrings. Here we go through those rules
# and convert those hexstrings to uppercase, since this is the standard we have set for the JSON messages.
suitable_for = []
for rule in self.module.suitable_for:
rule_filtered = {}
for k in rule:
if is_hexstr(rule[k]):
rule_filtered[k] = rule[k].upper()
else:
rule_filtered[k] = rule[k]
suitable_for.append(rule_filtered)
# Publish RCP Module description on the RCP server
tx_json = {'rcpm_hello':
{'name' : self.module.name,
'cmd_descr' : self.module.cmd_descr,
'suitable_for' : suitable_for,
'addr' : self.cmd_srv_addr,
'port' : self.cmd_srv_port
}
}
rx_json = await self._transact(tx_json)
if 'rcpm_welcome' not in rx_json:
raise ValueError("description not accepted by RCP Server")
class RcpModule(abc.ABC):
"""
Base class to implement to derive a concrete RCP module class
"""
# Module name used to identify the module in logs and user output. This module name should be short and concise.
name = "RCPM"
# Command description of this module. The command description consists of a short and concise command name, a
# helpstring and an argument specification in the form of a python dict. This specification, consisting of
# 'name', 'help', and 'args' is is directly passed to agparse on the client side.
#
# In addition to that, the API user may specify which keys the RCP Server shall retrieve before a command is
# executed. This is done via the 'get_keys' field. This field is optional and has the form of a dict with
# two optional fields 'uicc' and 'euicc'. The value part of both fields is a list of strings which name the
# columns that are passed to the CardKeyProvider for lookup. When the 'uicc' field is set, then the RCP Server
# will automatically request the ICCID from the card and do the lookup. When the 'euicc' field is set, the RCP
# Server will do the same with the EID. It is possible to mix both fields to request keys for the eUICC and the
# currently activated eSIM profile at the same time. However, this may be a very rare corner case.
#
# Example:
# cmd_descr = [{'name' : 'reset',
# 'help': 'reset the card',
# 'args' : []},
# {'name' : 'read_binary',
# 'help': 'read binary data from a transparent file.',
# 'args' : [{ 'name' : '--fid',
# 'spec' : {'required' : True,
# 'help' : 'File identifier to of the file to read',
# 'action' : 'append',
# 'pytype' : 'str'},
# }
# ]},
# {'name' : 'unlock_aram',
# 'help': 'unlock a locked ARA-M applet on a sysmoISIM-SJA5',
# 'args' : [],
# 'get_keys' : {'uicc' : ['KIC', 'KID', 'KIK']}}
# ]
cmd_descr = []
# Card properties to determine if this module is suitable for a specific card type or card types. The RCP Server
# will match those properties against user requests to determine which module provides useful services to the
# user's card.
#
# Example: [{"atr" : "3b9f96803f87828031e073fe211f574543753130136502"}]
suitable_for = []
# In addition the above, the derived class must implement command methods for each command that is defined in the
# command description (see above). Each command method must begin with the prefix "cmd_" followed by the command
# name used in the command description. A command method must have the form as shown in the example shown below.
# Each method should return an integer value which will become the final return code of the RCP client program.
#
# Args:
# hdlr: RcpModuleHdlr object, this object is provided by the RcpmCmdSrvConnHdlr object, which calls
# the command method of the module. Through the RcpModuleHdlr object, the API user gets access
# to special service methods (e.g. print) and other required properties (e.g. the SimCardCommands
# objects, key material and others (see RcpModuleHdlr).
#
# Example:
# def cmd_reset(self, hdlr: RcpModuleHdlr) -> int: ...
# def cmd_read_binary(self, hdlr: RcpModuleHdlr) -> int: ...
# def cmd_unlock_aram(self, hdlr: RcpModuleHdlr) -> int: ...
# When the RCP Module class is passed to rcpm_run_module(), rcpm_run_module() also accepts *args and **kwargs
# parameter. Those parameters are passed to the constructor of RCP Module class when it is instaniated by
# rcpm_run_module(). API may override this constructor (below) with a custom implementation, if required.
def __init__(self, *args, **kwargs):
pass
class RcpmCmdSrvConnHdlr(SrvSyncConnHdlr):
"""
The RCP Module command server connection handler is used to handle dedicated connections from the RCP Server. Those
dedicated connections are technically transparent connections between the RCP Client and the RCP Module (this). The
RCP Server merely acts as a proxy at that point.
"""
def __init__(self, module: RcpModule, field_cryptor: CardKeyFieldCryptor, *args, **kwargs):
SrvSyncConnHdlr.__init__(self, *args, *kwargs)
self.module = module
self.crypt = field_cryptor
def _parse_cmd_argv(self, cmd_suffix: str, cmd_argv: list[str]) -> Namespace:
""" Parse (and validate) the received argument vector """
# Use the cmd_descr of the module to create a (temporary) argument parser for the received argument vector.
cmd_parser = argparse.ArgumentParser()
for cmd in self.module.cmd_descr:
if cmd['name'] == cmd_suffix:
args = deepcopy(cmd['args'])
for arg in args:
arg['spec'] = pytype_to_type(arg['spec'])
cmd_parser.add_argument(arg['name'], **arg['spec'])
# Parse the arguments and return the parsed Namespace object.
try:
return cmd_parser.parse_args(cmd_argv)
except SystemExit:
raise ValueError("unable to parse arguments: %s", str(cmd_argv), )
def print(self, message: str):
""" Print a message on the client side """
log.info(str(self) + " -- %s" % message)
tx_json = {'rcps_instr': {'print' : message}}
rx_json = self._transact(tx_json)
if rx_json != {'rcps_result': {'empty' : None}}:
raise ValueError("unexpected response from RCP Client: %s", rx_json)
def procedure(self):
""" Receive and process a command from the RCP Client (via RCP Server) """
# Receive the command request.
rx_json = self._recv()
cmd = rx_json['rcps_command']['cmd']
cmd_argv = rx_json['rcps_command']['cmd_argv']
keys = rx_json['rcps_command'].get('keys')
log.info(str(self) + " -- executing command: %s %s", cmd, str(cmd_argv))
try:
# Make sure the command actually addresses this module.
cmd_prefix = self.module.name + "_"
if not cmd.startswith(cmd_prefix):
raise ValueError("invalid command: %s" % cmd)
# Make sure the module actually provides a command method for the requested command.
cmd_suffix = cmd[len(cmd_prefix):]
cmd_method = "cmd_" + cmd_suffix
if not hasattr(self.module, cmd_method):
raise ValueError("missing command method: %s" % cmd_method)
# Parse and validate command arguments.
cmd_args = self._parse_cmd_argv(cmd_suffix, cmd_argv)
# Setup a pySim RuntimeState, CardBase and a RuntimeLchan.
rs, card = init_card(RcpsSimLink(self))
# Hand over control to the command method provided by the specific module implementation.
rcp_module_hdlr = RcpModuleHdlr(self.print, rs, card, cmd_args, keys, self.crypt)
rs.reset()
try:
rc = getattr(self.module, cmd_method)(rcp_module_hdlr)
except Exception as e:
backtrace("command method")
rc = 1 # general error
except Exception as e:
backtrace("command parsing")
rc = 126 # cannot execute
# The prodedure is done, send "goodbye" message.
log.info(str(self) + " -- command execution done, rc: %d" % rc)
tx_json = {'rcps_goodbye': rc}
self._send(tx_json)
class RcpModuleHdlr():
"""
RCP Module handler class. This class is used by the RcpmCmdSrvConnHdlr to create the handler RcpModuleHdlr object
(hdlr), which is is passed to the command method. The RcpModuleHdlr gives the API user access to resources he can
use carry out the command.
"""
# The RuntimeState (rs), the CardBase (card) and the RuntimeLchan (lchan) are the three major objects through which
# an API user may interact with the UICC/eUICC on the other remote end. Those objects have the same objectives as
# in pySim-shell.py, with lchan representing the currently selected lchan (set to self.rs.lchan[0] by default, API
# users may change the reference to a different lchan)
rs = None
card = None
lchan = None
# The cmd_args property contains the parsed command arguments which were passed by the end-user to the RCP Client.
# The arguments are already parsed and validated against the cmd_dscr property of the RcpModule. The arguments are
# in the form of a Namespace object and can be accessed like any argparse output. However, since the arguments
# contain user input, some caution is required.
cmd_args = None
# In case the retrieve_uicc_keys property of the RcpModule is used retrieve UICC key material, this property will
# contain the key material in the form of a dictionary. The format is similar to the return value of
# card_key_provider_get() (see also pySim.card_key_provider).
keys_uicc = {}
# Same as self.keys_uicc, but contains eUICC related key material in case requested using retrieve_uicc_keys.
keys_euicc = {}
def __init__(self, print: callable, rs: RuntimeState, card: CardBase, cmd_args: Namespace,
keys: dict, field_cryptor: CardKeyFieldCryptor):
self.print = print
self.rs = rs
self.card = card
self.lchan = self.rs.lchan[0]
self.cmd_args = cmd_args
if keys:
if 'uicc' in keys:
self.keys_uicc = dict_from_key_value_pairs(keys['uicc'], keylabel='key', valuelabel='value')
for key in self.keys_uicc.keys():
self.keys_uicc[key] = field_cryptor.decrypt_field(key, self.keys_uicc.get(key))
if 'euicc' in keys:
self.keys_euicc = dict_from_key_value_pairs(keys['euicc'], keylabel='key', valuelabel='value')
for key in self.keys_euicc.keys():
self.keys_euicc[key] = field_cryptor.decrypt_field(key, self.keys_euicc.get(key))
def rcpm_setup_argparse(description: str):
"""Create argument parser and add the basic arguments all RCP Modules should have"""
option_parser = argparse.ArgumentParser(description='RCP Module: ' + description,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
option_parser.add_argument("--uri", help="URI of the RCP-Server", required=True)
option_parser.add_argument("--rcps-ca-cert", help="SSL/TLS CA-Certificate of the RCP-Server", required=True)
option_parser.add_argument("--rcpm-cmd-server-addr", help="Local Host/IP to bind RCP-Module-Command-Server to",
required=True)
option_parser.add_argument("--rcpm-cmd-server-port", help="Local TCP port to bind RCP-Module-Command-Server to",
required=True, type=int)
option_parser.add_argument("--rcpm-cmd-server-cert", help="SSL/TLS Certificate of the RCP-Module-Command-Server",
required=True)
CardKeyFieldCryptor.argparse_add_args(option_parser)
return option_parser
def rcpm_run_module(opts: Namespace, module: RcpModule, *args, **kwargs):
PySimLogger.setup(print, {logging.WARN: "\033[33m", logging.DEBUG: "\033[90m"}, opts.verbose)
log.info("RCP Module startup: %s", module.name)
log.debug("Main process ID: %d", os.getpid())
# Load SSL/TLS certificates.
rcpm_cmd_ssl_context = load_server_cert("RCPM Command Server", opts.rcpm_cmd_server_cert)
ssl_context = load_ca_cert("RCPM Server Client", opts.rcps_ca_cert)
# Load JSON schema for message validation between RCP Server and RCP Module (this process)
rcpm_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpm_to_rcps_schema.json"))
rcps_to_rcpm_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpm_schema.json"))
# Load JSON schema for message validation between RCP Server and RCP Module Command Server (this process)
rcpmcs_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcpmcs_to_rcps_schema.json"))
rcps_to_rcpmcs_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(), "rcps_to_rcpmcs_schema.json"))
# Start local RCP Client Command Server.
log.info("RCPC command server at: %s:%d" % (opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port))
def rcpm_cmd_conn_hdlr(websocket: ServerConnection):
json_validator = JsonValidator(rcps_to_rcpmcs_schema, rcpmcs_to_rcps_schema)
transport_keys = CardKeyFieldCryptor.transport_keys_from_opts(opts)
field_cryptor = CardKeyFieldCryptor(transport_keys)
hdlr = RcpmCmdSrvConnHdlr(module(*args, *kwargs), field_cryptor, websocket, RCP_SERVER_TIMEOUT, json_validator)
hdlr.procedure()
hdlr.close()
server = serve(rcpm_cmd_conn_hdlr, opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port, ssl=rcpm_cmd_ssl_context)
def rcpm_cmd_server():
log.debug("RCPC command server thread ID: %d", threading.get_native_id())
server.serve_forever()
rcpm_cmd_server_thread = threading.Thread(target = rcpm_cmd_server)
rcpm_cmd_server_thread.start()
# Connect to RCP Server and publish module description.
async def rcps_client():
async with websockets.connect(opts.uri, ping_timeout=10.0, ping_interval=1.0, ssl=ssl_context) as websocket:
json_validator = JsonValidator(rcps_to_rcpm_schema, rcpm_to_rcps_schema)
client = RcpsCltConnHdlr(opts.rcpm_cmd_server_addr, opts.rcpm_cmd_server_port, module, websocket,
RCP_SERVER_TIMEOUT, json_validator)
await client.check_version()
await client.describe()
await client.wait_close()
try:
asyncio.run(rcps_client())
except Exception as e:
backtrace("RCPS client")
# Shutdown
server.shutdown()
rcpm_cmd_server_thread.join()
log.info("RCP Module shutdown: %s", module.name)
+666
View File
@@ -0,0 +1,666 @@
#!/usr/bin/env python3
# (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import argparse
import asyncio
import logging
import time
import requests
import json
import websockets
from osmocom.utils import Hexstr
from pySim.utils import ResTuple
from copy import deepcopy
from pathlib import Path
from pySim.log import PySimLogger
from pySim.utils import dec_iccid
from websockets.asyncio.server import serve, ServerConnection
from rcp_utils import SrvConnHdlr, CltConnHdlr, JsonValidator, FlightRecorder
from rcp_utils import load_json_schema, backtrace, pytype_to_type, load_server_cert, load_ca_cert
from rcp_utils import key_value_pairs_from_dict
from pySim.card_key_provider import card_key_provider_argparse_add_args, card_key_provider_init
from pySim.card_key_provider import card_key_provider_get_field, card_key_provider_get
from packaging.version import Version
CLIENT_TIMEOUT = 10
# The protocol version between the RCP Server and the RCP Module must always match up. In case there as changes to
# the protocol (JSON Schema and/or application logic). This version number shall be incremented accordingly. Since
# RCP Modules usually run from the same pySim modules as the RCP Server, a change to this version number should
# not affect the RCP Module implementation itself.
RCPM_VERSION_PROTOCOL = "1.0.0"
# The RCP Server software version shall be incremented when there are changes to the RCP Sever (this module) or changes
# to other related modules, which affect the RCP Server. The RCP Server software version is also disclosed towards the
# RCP Client.
RCPS_VERSION_SOFTWARE = "1.0.0"
# The RCP Server protocol version refers to the protocol spoken between RCP Client and RCP Server. The protocol version
# shall be incremented when there are changes to the protocol (JSON Schema and/or application logic). When an
# RCP Client connects, this protocol version is compared against the protocol version that the client sends
# (see also RCPC_VERSION_PROTOCOL in rcp_client.py). It is up to the RCP Server to decide whether or not a deviation
# between protocol versions is tolerable or not.
RCPS_VERSION_PROTOCOL = "1.0.0"
log = PySimLogger.get(Path(__file__).stem)
runtime_state = None
rate_limiter = None
option_parser = argparse.ArgumentParser(description='RCP Server',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
option_parser.add_argument("--verbose", help="Enable verbose logging",
action='store_true', default=False)
option_parser.add_argument("--rcpc-server-addr", help="Local Host/IP to bind RCP-Client-Server to",
required=True)
option_parser.add_argument("--rcpc-server-port", help="Local TCP port to bind RCP-Client-Server to",
required=True, type=int)
option_parser.add_argument("--rcpc-server-cert", help="SSL/TLS Certificate of the RCP-Client-Server",
required=True)
option_parser.add_argument("--rcpc-request-limit", help="number of RCP Client requests per minute",
default=600)
option_parser.add_argument("--rcpm-server-addr", help="Local Host/IP to bind RCP-Module-Server to",
required=True)
option_parser.add_argument("--rcpm-server-port", help="Local TCP port to bind RCP-Module-Server to",
required=True, type=int)
option_parser.add_argument("--rcpm-server-cert", help="SSL/TLS Certificate of the RCP-Module-Server",
required=True)
option_parser.add_argument("--rcpm-module-ca-cert", help="SSL/TLS CA-Certificate of the RCP-Module-Command-Server",
required=True)
option_parser.add_argument("--open-observe-url", help="OpenObserve API endpoint URL")
option_parser.add_argument("--open-observe-email", help="OpenObserve service email address")
option_parser.add_argument("--open-observe-token", help="OpenObserve service token")
card_key_provider_argparse_add_args(option_parser)
class ModuleRuntimeState:
def __init__(self, websocket:ServerConnection, name:str, cmd_descr:list, suitable_for:list, addr:str, port:int):
self.name = name
self.websocket = websocket
# Run the cmd_descr through argparse to catch malformed argument specifications early
for cmd in cmd_descr:
args = deepcopy(cmd['args'])
cmd_parser = argparse.ArgumentParser()
for arg in args:
try:
arg['spec'] = pytype_to_type(arg['spec'])
cmd_parser.add_argument(arg['name'], **arg['spec'])
except:
raise ValueError("invalid argument spec %s -- check RCP Module" % str(arg))
self.cmd_descr = cmd_descr
self.suitable_for = suitable_for
self.addr = addr
self.port = port
log.debug("new RCP Module context created: '%s'", name)
def is_suitable(self, suitable_for:dict) -> bool:
"""Check if this module is 'suitable_for' a specific card"""
if suitable_for in self.suitable_for:
return True
return False
def describe(self) -> dict:
"""Describe this module towards the RCP Client"""
# The command description sent by the RCP Module also includes fields that are intended to be seen
# only by the RCP Server. Here we set up the command description as it is expected by the RCP Client.
cmd_descr = []
for descr in self.cmd_descr:
cmd_descr.append({'name' : descr['name'],
'help' : descr['help'],
'args' : descr['args']})
# Return module description
return {'name': self.name,
'cmd_descr': cmd_descr}
def get_cmd_descr(self, cmd: str) -> dict:
"""Get the description for a specific command of this module"""
for descr in self.cmd_descr:
if self.name + "_" + descr['name'] == cmd:
return descr
raise ValueError("command %s not found in command description %s" % (cmd_name, str(self.cmd_descr)))
def __str__(self) -> str:
return self.name
def __del__(self):
log.debug("RCP module context destroyed: '%s'", self.name)
class RuntimeState:
def __init__(self, rcpm_ca_ssl_context, open_observe_pars):
self.module_runtime_states = []
self.rcpm_ca_ssl_context = rcpm_ca_ssl_context
self.open_observe_pars = open_observe_pars
# Load JSON schema for message validation between RCP Client and RCP Server (this process)
self.rcpc_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
"rcpc_to_rcps_schema.json"))
self.rcps_to_rcpc_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
"rcps_to_rcpc_schema.json"))
# Load JSON schema for message validation between RCP Module and RCP Server (this process)
self.rcpm_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
"rcpm_to_rcps_schema.json"))
self.rcps_to_rcpm_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
"rcps_to_rcpm_schema.json"))
# Load JSON schema for message validation between RCP Module Command Server and RCP Server (this process)
self.rcpmcs_to_rcps_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
"rcpmcs_to_rcps_schema.json"))
self.rcps_to_rcpmcs_schema = load_json_schema(os.path.join(Path(__file__).parent.resolve(),
"rcps_to_rcpmcs_schema.json"))
log.debug("new runtime context created.")
def __log_modules_available(self) -> str:
if self.module_runtime_states:
modules_str = ""
for module in self.module_runtime_states:
modules_str += "'" + str(module) + "', "
return "RCP modules available: %s" % modules_str[:-2]
else:
return "RCP modules available: none"
def module_add(self, module: ModuleRuntimeState):
self.module_runtime_states.append(module)
log.info("new RCP module, %s", self.__log_modules_available())
def module_remove(self, websocket:ServerConnection):
for module in self.module_runtime_states:
if module.websocket == websocket:
self.module_runtime_states.remove(module)
log.info("RCP module removed, %s", self.__log_modules_available())
return
log.warning("cannot remove RCP module, no RCP module associated with RCPC connection: %s:%d, %s" %
(*websocket.remote_address, self.__log_modules_available()))
def modules_find(self, suitable_for:dict) -> list[dict]:
modules = []
for module in self.module_runtime_states:
if module.is_suitable(suitable_for):
modules.append(module.describe())
if modules:
return modules
# It is absolutely tolerable if no suitable RCP module can be found. If this is the case, the client should
# display an empty help screen and exit normally.
log.warning("no suitable RCP module found, %s", self.__log_modules_available())
return []
def module_find(self, suitable_for:dict, cmd:str) -> ModuleRuntimeState:
modules = self.modules_find(suitable_for)
for m in modules:
module_name = m['name']
cmd_descr = m['cmd_descr']
for c in cmd_descr:
cmd_name = c['name']
if module_name + "_" + cmd_name == cmd:
break
for module_runtime_state in self.module_runtime_states:
if module_runtime_state.name == module_name:
return module_runtime_state
# Normally we should find the RCP module. When this method is called, we have already called modules_find
# before because we had to return the command descriptions to the client. If we cannot find the RCP module
# now, the module have been disconnected or the client somehow called a command that does not exist. In any
# case, ending up here means we cannot continue.
raise ValueError("RCP module not found for command: %s, " % (cmd, self.__log_modules_available()))
class RcpmCltConnHdlr(CltConnHdlr):
"""
The RCP Module client connection handler is the dedicated client that is used by the RCP Client connection handler
to handle the dedicated connection towards the RCP Module (see below)
"""
class RcpcSrvConnHdlr(SrvConnHdlr):
"""
The RCP Client connection handler takes care of the handling of client requests. Throughout the lifetime of a
connection, the client will request a description of the available commands and then request the execution of a
procedure. To execute the procedure, the handler will make a dedicated connection to the RCP Module and then
transparently pass the messages from the RCP Client to the RCP Module and vice versa.
"""
module_client = None
async def check_version(self):
"""
Check the RCP Client software and protocol version to ensure the requesting RCP Client is compatible with this
RCP Server version.
"""
# Receive version info from RCP client
rx_json = await self._recv()
rcpc_version_software = Version(rx_json['rcpc_version']['software'])
rcpc_version_protocol = Version(rx_json['rcpc_version']['protocol'])
log.debug("RCP Client version: software=%s, protocol=%s",
rcpc_version_software, rcpc_version_protocol)
if self.flight_recorder:
self.flight_recorder.record_meta('rcpc_version_software', str(rcpc_version_software))
self.flight_recorder.record_meta('rcpc_version_protocol', str(rcpc_version_protocol))
# Check if the RCP Client is compatible with this RCP Server. As of now we expect that the client uses the
# exact same protocol version as the server.
rcpc_version_protocol_expected = Version(RCPS_VERSION_PROTOCOL)
if rcpc_version_protocol != rcpc_version_protocol_expected:
info = "RCP Client uses unsupported protocol version (%s != %s)" % (rcpc_version_protocol, rcpc_version_protocol_expected)
raise_exception = True
else:
info = None
raise_exception = False
# Respond with RCP Server version info. We do this before we potentially raise an exception to make sure the
# RCP Server version info arrives at the client.
tx_json = {'rcpc_version': {'software' : RCPS_VERSION_SOFTWARE,
'protocol' : RCPS_VERSION_PROTOCOL}}
if info:
tx_json['rcpc_version']['info'] = info
await self._send(tx_json)
# Raise exception in case problems were detected. This will close the connection, but the client still has the
# version info (see above)
if raise_exception:
raise ValueError(info)
async def describe(self):
"""
Collect the command/argument description of suitable modules and forward that definition to the RCP client. The
RCP client will then build an argument parser (commandline help, argument validation) from this information.
"""
rx_json = await self._recv()
self.suitable_for = rx_json['rcpc_hello']['suitable_for']
if self.flight_recorder:
self.flight_recorder.record_meta('suitable_for', self.suitable_for)
modules = runtime_state.modules_find(self.suitable_for)
if self.flight_recorder:
suitable_modules = []
for m in modules:
suitable_modules.append(m['name'])
self.flight_recorder.record_meta('suitable_modules', suitable_modules)
tx_json = {'rcpc_welcome':
{'module_descr' : modules}
}
await self._send(tx_json)
async def _transact_apdu(self, apdu: Hexstr) -> ResTuple:
"""Private low level method to exchange an APDU"""
tx_json = {'rcpc_instr': {'c_apdu' : apdu.upper()}}
rx_json = await self._transact(tx_json)
if rx_json is None:
raise ValueError("RCP Client vanished unexpectetly")
data = rx_json['rcpc_result']['r_apdu']['data']
sw = rx_json['rcpc_result']['r_apdu']['sw']
return data, sw
async def _reset(self) -> Hexstr:
"""Private low level method to reset the UICC/eUICC"""
tx_json = {'rcpc_instr': {'reset' : None}}
rx_json = await self._transact(tx_json)
if rx_json is None:
raise ValueError("RCP Client vanished unexpectetly")
return rx_json['rcpc_result']['atr']
async def _read_iccid(self) -> Hexstr:
"""Private low level method to read the EID from an UICC (or eSIM)"""
data, sw = await self._transact_apdu("00A40000022FE200")
if sw != "9000":
raise ValueError("Unable to select EF.ICCID, sw: %s, " % sw)
data, sw = await self._transact_apdu("00B000000A")
if sw != "9000":
raise ValueError("Unable to read EF.ICCID, sw: %s, " % sw)
return dec_iccid(data)
async def _read_eid(self) -> Hexstr:
"""Private low level method to read the EID from an eUICC"""
data, sw = await self._transact_apdu("00A4040410A0000005591010FFFFFFFF890000010000")
if sw != "9000":
raise ValueError("Unable to select ISD-R, sw: %s, " % sw)
data, sw = await self._transact_apdu("80E2910006BF3E035C015A00")
if sw != "9000":
raise ValueError("Unable to retrieve EID, sw: %s, " % sw)
return data[10:]
async def print(self, message: str):
""" Print a message on the client side """
tx_json = {'rcpc_instr': {'print' : message}}
rx_json = await self._transact(tx_json)
if rx_json is None:
raise ValueError("RCP Client vanished unexpectedly")
if rx_json != {'rcpc_result': {'empty' : None}}:
raise ValueError("unexpected response from RCP Client: %s" % rx_json)
async def procedure(self):
"""
Receive a command from the client, pick a matching module, make a dedicated connection to that module and
forward instruction/response messages between RCP Client and RCP Module until the procedure is done.
"""
# Receive a command from the client.
rx_json = await self._recv()
# The procedure step is not mandatory. In case no procedure shall be executed, the client may close the
# connection early on his behalf. This is normal behavior and usually the case when the user instructs the
# RCP client to display the commandline help screens.
if rx_json is None:
log.debug(str(self) + " -- RCP client has closed the connection, no procedure executed")
return
# The RCP client has sent a command, so we continue with the procedure.
command = rx_json['rcpc_command']
if self.flight_recorder:
self.flight_recorder.record_meta('cmd', command['cmd'])
self.flight_recorder.record_meta('cmd_argv', command['cmd_argv'])
# Pick the matching RCP Module
module = runtime_state.module_find(self.suitable_for, command['cmd'])
if self.flight_recorder:
self.flight_recorder.record_meta('module', module.name)
# Retrieve keys (if the command requires them)
cmd_descr = module.get_cmd_descr(command['cmd'])
get_keys = cmd_descr.get('get_keys')
if get_keys:
keys = {}
get_keys_uicc = get_keys.get('uicc')
if get_keys_uicc:
iccid = await self._read_iccid()
if self.flight_recorder:
self.flight_recorder.record_meta('iccid', iccid)
keys_uicc = card_key_provider_get(get_keys_uicc, 'ICCID', iccid)
keys['uicc'] = key_value_pairs_from_dict(keys_uicc, keylabel='key', valuelabel='value')
get_keys_euicc = get_keys.get('euicc')
if get_keys_euicc:
eid = await self._read_eid()
if self.flight_recorder:
self.flight_recorder.record_meta('eid', eid)
keys_euicc = card_key_provider_get(get_keys_euicc, 'EID', eid)
keys['euicc'] = key_value_pairs_from_dict(keys_euicc, keylabel='key', valuelabel='value')
command['keys'] = keys
# Resetting card to ensure the card is in a defined state
await self._reset()
# Create a dedicated connection to the RCP Module and proxy the messages between RCP Client and RCP Module.
module_uri = "wss://%s:%d" % (module.addr, module.port)
log.info(str(self) + " -- executing procedure for command \"%s\" on module \"%s\" at: %s" %
(command['cmd'], module.name, module_uri))
async with websockets.connect(module_uri, ssl=runtime_state.rcpm_ca_ssl_context) as websocket:
# Create a connection to the RCP Module Command Server
json_validator = JsonValidator(runtime_state.rcpmcs_to_rcps_schema, runtime_state.rcps_to_rcpmcs_schema)
self.module_client = RcpmCltConnHdlr(websocket, CLIENT_TIMEOUT, json_validator, self.flight_recorder)
# Prepare initial request to be send to the RCP Module Command Server
module_tx_json = {'rcps_command' : command}
# Forward messages between RCP Module Command Server and RCP Client until the procedure ends.
while(True):
# Send request to the RCP Module Command Server
module_rx_json = await self.module_client._transact(module_tx_json)
# Forward the response to the RCP Client
if 'rcps_instr' in module_rx_json:
client_tx_json = {'rcpc_instr' : module_rx_json['rcps_instr']}
await self._send(client_tx_json)
elif 'rcps_goodbye' in module_rx_json:
rc = module_rx_json['rcps_goodbye']
log.info(str(self) + " -- command execution done, rc: %d" % rc)
if self.flight_recorder:
self.flight_recorder.record_meta('rc', rc)
if rc != 0:
self.flight_recorder.crash_report()
client_tx_json = {'rcpc_goodbye' : rc}
await self._send(client_tx_json)
break
else:
raise ValueError("Unexpected response from RCP Module: %s" % str(module_rx_json))
# Receive the Result from the client, prepare request (module_tx_json) for the next turn
client_rx_json = await self._recv()
if client_rx_json is None:
raise ValueError("RCP client vanished unexpectedly")
if 'rcpc_result' in client_rx_json:
module_tx_json = {'rcps_result' : client_rx_json['rcpc_result']}
else:
raise ValueError("Unexpected result from RCP Client: %s" % str(client_rx_json))
async def close(self):
"""
Close the connection towards the RCP Module Command Server, then close the connection towards the RCP Client.
"""
if self.module_client:
await self.module_client.close()
await super().close()
class RcpmSrvConnHdlr(SrvConnHdlr):
"""
The RCP Module connection handler is responsible to handle connect and disconnect events of RCP Modules. This
connection between the RCP Module and the RCP Server is used for management purposes only.
"""
async def check_version(self):
"""
Send the Protocol and Software version of this RCP Module to the RCP Server. The RCP Server and the RCP Module
must always use the same protocol version.
"""
tx_json = {'rcpm_version': {'protocol' : RCPM_VERSION_PROTOCOL}}
rx_json = await self._transact(tx_json)
rcpm_version_protocol = Version(rx_json['rcpm_version']['protocol'])
if Version(RCPM_VERSION_PROTOCOL) != rcpm_version_protocol:
raise ValueError("Incompatible protocol version %s != %s", Version(RCPM_VERSION_PROTOCOL), rcpm_version_protocol)
async def describe(self):
"""
Receive the module description from an RCP Module. This description will be stored in an internal list until
the module is disconnected from the server.
"""
rx_json = await self._recv()
runtime_state.module_add(module = ModuleRuntimeState(self.websocket, **rx_json['rcpm_hello']))
tx_json = {'rcpm_welcome': None}
await self._send(tx_json)
def __del__(self):
"""
Remove RCPM from internal list when the connection is closed (and the handler is deleted)
"""
runtime_state.module_remove(self.websocket)
super().__del__()
class RateLimiter():
"""
Rate limiter: A rate limiter can be used to limit the amount of requests
per interval. Once the interval expires, the request counter is reset and
the requestor gets a new request budget to spend.
"""
def __init__(self, interval:int, requests:int):
"""
Args:
interval: reset interval after which request counter is reset.
requests: maximum number of requests per interval.
Returns:
True when rate limit has been exceeded, False otherwise.
"""
self.table = {}
self.interval = interval
self.requests = requests
self.last_collect = time.time()
log.info("Rate-Limit: max %d requests per sec.", self.requests / self.interval)
def __collect_expired(self):
new_table = {}
for key in self.table.keys():
if time.time() - self.table[key]['timestamp'] <= self.interval:
new_table[key] = self.table[key]
self.table = new_table
def limit(self, address:str) -> bool:
"""
Rate limit request
Args:
address: requestor address
Returns:
True when rate limit has been exceeded, False otherwise
"""
timestamp = time.time()
# Collect expired entries once per minute
if time.time() - self.last_collect > 60:
self.__collect_expired()
self.last_collect = timestamp
# In case no entry exists yet, create a new one => don't block
if address not in self.table:
self.table[address] = {'timestamp' : timestamp, 'counter' : 1}
log.debug("Rate-Limit: %s (new, counter=%d, next reset in %d sec.)",
address, 1, self.interval)
return False
# We have to access multiple times, so its better to story the entry
# in a temporary variable.
entry = self.table[address]
# If the entry has expired - delete it => don't block
if timestamp - entry['timestamp'] > self.interval:
log.debug("Rate-Limit: %s (reset, counter=%d, next reset in %d sec.)",
address, 1, self.interval)
self.table[address] = {'timestamp' : timestamp, 'counter' : 1}
return False
# If the rate limit has been reached => block
if entry['counter'] >= self.requests:
log.warning("Rate-Limit: %s (exceeded, counter=%d, next reset in %d sec.)",
address, entry['counter'], self.interval - (timestamp - entry['timestamp']))
return True
# Increment counter, don't block
entry['counter'] += 1
log.debug("Rate-Limit: %s (incrementing, counter=%d, next reset in %d sec.)",
address, entry['counter'], self.interval - (timestamp - entry['timestamp']))
self.table[address] = entry
return False
class OpenObserveFlightRecorder(FlightRecorder):
"""Concrete implementation of a "flight recorder" using OpenObserve as a monitoring entity."""
def __init__(self, url: str, email: str, token: str):
self.service_auth = requests.auth.HTTPBasicAuth(email, token)
self.url = url
super().__init__()
def report(self):
report_json = json.dumps(self._gen_report())
rc = requests.post(self.url, auth=self.service_auth, data=report_json)
if rc.status_code != 200:
log.error("POST request to OpenObserve failed: %s", str(rc))
async def rcpc_conn_hdlr(websocket: ServerConnection):
"""
In this handler function we process the request from the the RCP Client. Before we perform any action we check if
the rate limit is not exceeded. Then we describe the available commands to the client and execute the procedure
the client asks for. When everything is done we close the connection normally. The client may skip executing any
procedure by closing the connection early on his behalf.
The interaction with the client is recorded using a "flight recorder" object. When the interaction is done, the
records are analyzed and a report is generated and sent to the OpenObserve monitoring entity.
"""
# Immediately close the connection in case the rate limit has been exceeded.
if rate_limiter.limit(websocket.remote_address[0]):
await websocket.close(code=1008) # Policy Violation
# Create flight-recorder object
flight_recorder = None
if runtime_state.open_observe_pars:
flight_recorder = OpenObserveFlightRecorder(**runtime_state.open_observe_pars)
# Execute procedure
try:
json_validator = JsonValidator(runtime_state.rcpc_to_rcps_schema, runtime_state.rcps_to_rcpc_schema)
hdlr = RcpcSrvConnHdlr(websocket, CLIENT_TIMEOUT, json_validator, flight_recorder)
await hdlr.check_version()
await hdlr.describe()
await hdlr.procedure()
await hdlr.close()
except Exception as e:
backtrace("RCPC connection handler")
if flight_recorder:
flight_recorder.record_backtrace()
flight_recorder.crash_report()
await websocket.close(code=1011) # Internal Error
# Generate report from flight-recorder
if flight_recorder:
flight_recorder.report()
async def rcpm_conn_hdlr(websocket: ServerConnection):
"""
In this handler function we process requests from the RCP Module. We receive the description from the RCP Module.
We keep the connection open throughout the whole lifetime of the RCP Module process so that we can know when the
RCP Module becomes unavailable for some reason.
"""
try:
json_validator = JsonValidator(runtime_state.rcpm_to_rcps_schema, runtime_state.rcps_to_rcpm_schema)
hdlr = RcpmSrvConnHdlr(websocket, CLIENT_TIMEOUT, json_validator)
await hdlr.check_version()
await hdlr.describe()
await hdlr.close()
except:
backtrace("RCPM connection handler")
if __name__ == '__main__':
opts = option_parser.parse_args()
PySimLogger.setup(print, {logging.WARN: "\033[33m", logging.DEBUG: "\033[90m"}, opts.verbose)
# Load SSL/TLS certificates
rcpc_ssl_context = load_server_cert("RCP Client Server", opts.rcpc_server_cert)
rcpm_ssl_context = load_server_cert("RCP Module Server", opts.rcpm_server_cert)
rcpm_ca_ssl_context = load_ca_cert("RCP Module Command Server Client", opts.rcpm_module_ca_cert)
# Init card key provider for automatic card key retrieval
card_key_provider_init(opts)
# Prepare parameters for OpenObserve
if opts.open_observe_url and opts.open_observe_email and opts.open_observe_token:
open_observe_pars = {'url' : opts.open_observe_url,
'email': opts.open_observe_email,
'token' : opts.open_observe_token}
log.info("Reporting to OpenObserve: %s", open_observe_pars['url'])
else:
log.warning("Reporting to OpenObserve: (disabled)")
open_observe_pars = None
# Start RCP server
runtime_state = RuntimeState(rcpm_ca_ssl_context, open_observe_pars)
rate_limiter = RateLimiter(interval=60, requests=opts.rcpc_request_limit)
async def rcp_server():
log.info("RCP Client Server at: %s:%d" % (opts.rcpc_server_addr, opts.rcpc_server_port))
log.info("RCP Module server at: %s:%d" % (opts.rcpm_server_addr, opts.rcpm_server_port))
async with serve(rcpc_conn_hdlr, opts.rcpc_server_addr, opts.rcpc_server_port, ssl=rcpc_ssl_context), \
serve(rcpm_conn_hdlr, opts.rcpm_server_addr, opts.rcpm_server_port, ssl=rcpm_ssl_context):
await asyncio.get_running_loop().create_future()
try:
asyncio.run(rcp_server())
except SystemExit:
pass
except:
backtrace("RCP Server")
sys.exit(1)
+330
View File
@@ -0,0 +1,330 @@
#!/usr/bin/env python3
# (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import ssl
import json
import abc
import asyncio
import time
import websockets
import traceback
import threading
from copy import deepcopy
from websockets.asyncio.server import ServerConnection
from websockets.asyncio.client import ClientConnection
from pathlib import Path
from jsonschema import validate
from pySim.log import PySimLogger
from ssl import SSLContext
log = PySimLogger.get(Path(__file__).stem)
def backtrace(what: str):
log.error("%s failed with an exception:", what)
log.error("---------------------8<---------------------")
traceback_lines = traceback.format_exc()
for line in traceback_lines.split("\n"):
if line:
log.error(line)
log.error("---------------------8<---------------------")
def key_value_pairs_from_dict(keys: dict, keylabel: str='key', valuelabel: str='value') -> list:
key_list = []
for key in keys:
key_list.append({keylabel : key, valuelabel : keys[key]})
return key_list
def dict_from_key_value_pairs(keys: list, keylabel: str='key', valuelabel: str='value') -> dict:
key_dict = {}
for key in keys:
key_dict[key[keylabel]] = key[valuelabel]
return key_dict
def pytype_to_type(dict_in: dict) -> dict:
"""
There is no way to properly express python types in JSON. This function can be used to replace
each occurrence of "pytype", with "type", where the string type name is replaced with an actual
python type.
"""
dict_out = deepcopy(dict_in)
if dict_out.get('pytype'):
if dict_out['pytype'] == "str":
dict_out.pop('pytype')
dict_out['type'] = str
elif dict_out['pytype'] == "int":
dict_out.pop('pytype')
dict_out['type'] = int
else:
raise ValueError("invalid type in command argument specification: %s" % arg['spec']['type'])
return dict_out
def load_json_schema(filename: str) -> dict:
"""Load a JSON schema from file"""
log.debug("loading JSON schema: %s", filename)
try:
with open(filename) as schema_file:
return json.load(schema_file)
except Exception as e:
backtrace("JSON schema load")
sys.exit(1)
def load_server_cert(what: str, filename: str) -> SSLContext:
"""Load an SSL/TLS server certificate"""
log.debug("loading SSL/TLS server certificate (%s): %s", what, filename)
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(filename)
return ssl_context
def load_ca_cert(what: str, filename: str) -> SSLContext:
"""Load an SSL/TLS CA certificate"""
log.info("loading SSL/TLS CA certificate (%s): %s", what, filename)
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.load_verify_locations(filename)
return ssl_context
class JsonValidator():
"""
JSON validator class, can be passed to any ConnHdlr object to automatically validate the JSON messages which are
sent and and received.
"""
def __init__(self, rx_schema: dict, tx_schema: dict = None):
self.rx_schema = rx_schema
if tx_schema:
self.tx_schema = tx_schema
else:
self.tx_schema = None
def valid_rx_json(self, rx_json: dict):
validate(instance = rx_json, schema = self.rx_schema)
def valid_tx_json(self, tx_json: dict):
if self.tx_schema:
# We intentionally do not prevent the sending of an invalid JSON message. It is the responsibility of the
# receiving end to detect an invalid message and react accordingly. The purpose of this validation is to
# make developers/users aware of the problem.
try:
validate(instance = tx_json, schema = self.tx_schema)
except Exception as e:
backtrace("JSON schema validation (TX)")
class FlightRecorder(abc.ABC):
"""
Base class to create a FlightRecorder object which can be passed to any ConnHdlr object to record debug information
(record_comm, record_debug) and metadata (record_meta) throughout the lifetime of a ConnHdlr object. In case the
ConnHdlr throws an exception, the API user may call the record_backtrace method to record a backtrace. Finally,
the APU user may call the report method (which calls _gen_report internally) to send a report to an external
monitoring enitiy.
"""
def __init__(self):
self.records_meta = {}
self.records_comm = []
self.records_debug = []
self.crash_report_flag = False
self.record_meta('timestamp_start', time.strftime('%Y-%m-%d %H:%M:%S'))
def record_meta(self, key: str, value):
"""Record/Update metadata"""
self.records_meta[key] = value
def record_comm(self, key: str, value):
"""Record communication (automatically called by the ConnHdlr object)"""
self.records_comm.append({key : value})
def record_debug(self, key: str, value):
"""Record debug information"""
self.records_debug.append({key : value})
def record_backtrace(self):
"""Record a backtrace"""
traceback_lines = traceback.format_exc()
traceback_lines_filtered = []
for line in traceback_lines.split("\n"):
if line:
traceback_lines_filtered.append(line)
self.record_debug('backtrace', traceback_lines_filtered)
def crash_report(self):
"""Set crash_report_flag. Thie method shall be called if an unrecoverable error has occured."""
self.crash_report_flag = True
def _gen_report(self):
"""
Generate a report from the collected data. In case the crash_report flag is set to true, the report will
inculde communications (records_comm) and debug information (records_debug). Otherwise only the metadata
(records_meta) will be included.
"""
self.record_meta('timestamp_end', time.strftime('%Y-%m-%d %H:%M:%S'))
report = self.records_meta
if self.crash_report_flag:
report['comm'] = self.records_comm
report['debug'] = self.records_debug
report['report_type'] = 'crash'
log.warning("crash report: %s", str(report))
return report
else:
report['report_type'] = 'normal'
log.debug("normal report: %s", str(report))
return report
@abc.abstractmethod
def report(self):
"""
To be implemented in the derived class. Shall call _gen_report and then send the report to an external
monitoring entity.
"""
pass
class ConnHdlr(abc.ABC):
"""Base class that can be used to create a connection handler"""
def __init__(self, websocket: ServerConnection | ClientConnection, timeout: int,
json_validator: JsonValidator = None, flight_recorder: FlightRecorder = None):
self.websocket = websocket
self.local_address = websocket.local_address
self.remote_address = websocket.remote_address
self.timeout = timeout
self.json_validator = json_validator
self.flight_recorder = flight_recorder
log.debug(str(self) + " -- new handler, timeout: %d sec.", self.timeout)
if self.flight_recorder:
self.flight_recorder.record_meta(type(self).__name__ + '_remote_address',
str(self.remote_address[0]) + ":" + str(self.remote_address[1]))
self.flight_recorder.record_meta(type(self).__name__ + '_timestamp', time.strftime('%Y-%m-%d %H:%M:%S'))
self.flight_recorder.record_meta(type(self).__name__ + '_id', id(self))
def _log_recv_peer(self, rx_json_str: str):
peer = "%s:%d<-%s:%d" % (self.local_address[0],
self.local_address[1],
self.remote_address[0],
self.remote_address[1])
log.debug(str(self) + " -- RX(%s): %s", peer, rx_json_str)
if self.flight_recorder:
self.flight_recorder.record_comm(type(self).__name__ + '_rx', rx_json_str)
def _log_send_peer(self, tx_json_str: str):
peer = "%s:%d->%s:%d" % (self.local_address[0],
self.local_address[1],
self.remote_address[0],
self.remote_address[1])
log.debug(str(self) + " -- TX(%s): %s", peer, tx_json_str)
if self.flight_recorder:
self.flight_recorder.record_comm(type(self).__name__ + '_tx', tx_json_str)
def __str__(self) -> str:
return "%s(%d)" % (type(self).__name__, id(self))
def __del__(self):
log.debug(str(self) + " -- closed handler")
class SrvConnHdlr(ConnHdlr):
"""Base class that can be used to create a connection handler for a server"""
async def _recv(self) -> dict:
"""Receive JSON message from client"""
async with asyncio.timeout(self.timeout):
try:
rx_json_str = await self.websocket.recv()
except websockets.exceptions.ConnectionClosedOK:
log.debug(str(self) + " -- no data received, connection is closed")
return None
self._log_recv_peer(rx_json_str)
rx_json = json.loads(rx_json_str)
if self.json_validator:
self.json_validator.valid_rx_json(rx_json)
return rx_json
async def _send(self, tx_json: dict):
"""Send JSON message to client"""
if self.json_validator:
self.json_validator.valid_tx_json(tx_json)
tx_json_str = json.dumps(tx_json)
self._log_send_peer(tx_json_str)
await self.websocket.send(tx_json_str)
async def _transact(self, tx_json: dict) -> dict:
"""Exchange JSON message with client"""
await self._send(tx_json)
return await self._recv()
async def close(self):
"""Wait for a connecion to close normally"""
await self.websocket.wait_closed()
log.debug(str(self) + " -- closed connection")
class SrvSyncConnHdlr(ConnHdlr):
"""Base class that can be used to create a synchronous connection handler for a server"""
def _recv(self) -> dict:
"""Receive JSON message from client"""
rx_json_str = self.websocket.recv(self.timeout)
self._log_recv_peer(rx_json_str)
rx_json = json.loads(rx_json_str)
if self.json_validator:
self.json_validator.valid_rx_json(rx_json)
return rx_json
def _send(self, tx_json: dict):
"""Send JSON message to client"""
if self.json_validator:
self.json_validator.valid_tx_json(tx_json)
tx_json_str = json.dumps(tx_json)
self._log_send_peer(tx_json_str)
self.websocket.send(tx_json_str)
def _transact(self, tx_json: dict) -> dict:
"""Exchange JSON message with client"""
self._send(tx_json)
return self._recv()
def close(self):
"""Close connection normally"""
self.websocket.close()
log.debug(str(self) + " -- closed connection")
class CltConnHdlr(ConnHdlr):
"""Base class that can be used to create a connection handler for a client"""
async def _transact(self, tx_json: dict) -> dict:
"""Exchange JSON message with server"""
if self.json_validator:
self.json_validator.valid_tx_json(tx_json)
tx_json_str = json.dumps(tx_json)
self._log_send_peer(tx_json_str)
async with asyncio.timeout(self.timeout):
await self.websocket.send(tx_json_str)
rx_json_str = await self.websocket.recv()
self._log_recv_peer(rx_json_str)
rx_json = json.loads(rx_json_str);
if self.json_validator:
self.json_validator.valid_rx_json(rx_json)
return rx_json
async def close(self):
"""Close connection normally"""
await self.websocket.close()
log.debug(str(self) + " -- closed connection")
async def wait_close(self):
"""Wait for a connecion to close normally"""
await self.websocket.wait_closed()
log.debug(str(self) + " -- closed connection")
+99
View File
@@ -0,0 +1,99 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RCP Client to RCP Server",
"type": "object",
"properties": {
"rcpc_version": {
"type": "object",
"properties": {
"software": {
"type": "string"
},
"protocol": {
"type": "string"
}
},
"required": [ "software", "protocol" ],
"additionalProperties": false
},
"rcpc_hello": {
"type": "object",
"properties": {
"suitable_for": {
"type": "object",
"properties": {
"atr": {
"type": "string",
"pattern": "^[0-9,A-F]{0,66}$"
}
},
"oneOf": [
{ "required": [ "atr" ] }
],
"additionalProperties": false
}
},
"required": [ "suitable_for" ],
"additionalProperties": false
},
"rcpc_command": {
"type": "object",
"properties": {
"cmd": {
"type": "string",
"pattern": "^[0-9,A-Z,a-z,_]{0,40}$"
},
"cmd_argv": {
"type": "array",
"items": {
"type": "string",
"pattern": "^.{0,512}$"
},
"maxItems": 255
}
},
"required": [ "cmd", "cmd_argv" ],
"additionalProperties": false
},
"rcpc_result": {
"type": "object",
"properties": {
"r_apdu": {
"type": "object",
"properties": {
"data": {
"type": "string",
"pattern": "^[0-9,A-F]{0,512}$"
},
"sw": {
"type": "string",
"pattern": "^[0-9,A-F]{0,4}$"
}
},
"required": [ "data", "sw" ],
"additionalProperties": false
},
"atr": {
"type": "string",
"pattern": "^[0-9,A-F]{0,66}$"
},
"empty": {
"type": "null"
}
},
"oneOf": [
{ "required": [ "r_apdu" ] },
{ "required": [ "atr" ] },
{ "required": [ "empty" ] }
],
"additionalProperties": false
}
},
"oneOf": [
{ "required": [ "rcpc_version" ] },
{ "required": [ "rcpc_hello" ] },
{ "required": [ "rcpc_command" ] },
{ "required": [ "rcpc_result" ] }
],
"additionalProperties": false
}
+127
View File
@@ -0,0 +1,127 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RCP Module to RCP Server",
"type": "object",
"properties": {
"rcpm_version": {
"type": "object",
"properties": {
"protocol": {
"type": "string"
}
},
"required": [ "protocol" ],
"additionalProperties": false
},
"rcpm_hello": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"cmd_descr": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"help": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"spec": {
"type": "object",
"properties": {
"required" : {
"type": "boolean"
},
"help": {
"type": "string"
},
"action": {
"type": "string"
},
"pytype": {
"type": "string"
},
"default" : {
"type": ["string", "integer"]
}
},
"required": [ "help" ],
"additionalProperties": false
}
},
"required": [ "name", "spec" ],
"additionalProperties": false
}
},
"get_keys": {
"type": "object",
"properties": {
"uicc" : {
"type": "array",
"items": {
"type": "string"
}
},
"euicc" : {
"type": "array",
"items": {
"type": "string"
}
}
},
"oneOf": [
{ "required": [ "uicc" ] },
{ "required": [ "euicc" ] }
],
"additionalProperties": false
}
},
"required": [ "name", "help", "args" ],
"additionalProperties": false
}
},
"suitable_for": {
"type": "array",
"items": {
"type": "object",
"properties": {
"atr": {
"type": "string",
"pattern": "^[0-9,A-F]{0,66}$"
}
},
"oneOf": [
{ "required": [ "atr" ] }
],
"additionalProperties": false
}
},
"addr": {
"type": "string"
},
"port": {
"type": "integer"
}
},
"required": [ "name", "cmd_descr", "suitable_for", "addr", "port" ],
"additionalProperties": false
}
},
"oneOf": [
{ "required": [ "rcpm_hello" ] },
{ "required": [ "rcpm_version" ] }
],
"additionalProperties": false
}
+36
View File
@@ -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
}
+125
View File
@@ -0,0 +1,125 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RCP Server to RCP Client",
"type": "object",
"properties": {
"rcpc_version": {
"type": "object",
"properties": {
"software": {
"type": "string"
},
"protocol": {
"type": "string"
},
"info": {
"type": "string"
}
},
"required": [ "software", "protocol" ],
"additionalProperties": false
},
"rcpc_welcome": {
"type": "object",
"properties": {
"module_descr": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"cmd_descr": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"help": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"spec": {
"type": "object",
"properties": {
"required" : {
"type": "boolean"
},
"help": {
"type": "string"
},
"action": {
"type": "string"
},
"pytype": {
"type": "string"
},
"default" : {
"type": ["string", "integer"]
}
},
"required": [ "help" ],
"additionalProperties": false
}
},
"required": [ "name", "spec" ],
"additionalProperties": false
}
}
},
"required": [ "name", "help", "args" ],
"additionalProperties": false
}
}
},
"required": [ "name", "cmd_descr" ],
"additionalProperties": false
}
}
},
"required": [ "module_descr" ],
"additionalProperties": false
},
"rcpc_instr": {
"type": "object",
"properties": {
"print": {
"type": "string"
},
"reset": {
"type": "null"
},
"c_apdu": {
"type": "string",
"pattern": "^[0-9,A-F]{0,512}$"
}
},
"oneOf": [
{ "required": [ "print" ] },
{ "required": [ "reset" ] },
{ "required": [ "c_apdu" ] }
],
"additionalProperties": false
},
"rcpc_goodbye": {
"type": "integer"
}
},
"oneOf": [
{ "required": [ "rcpc_version" ] },
{ "required": [ "rcpc_welcome" ] },
{ "required": [ "rcpc_instr" ] },
{ "required": [ "rcpc_goodbye" ] }
],
"additionalProperties": false
}
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RCP Server to RCP Module",
"type": "object",
"properties": {
"rcpm_version": {
"type": "object",
"properties": {
"protocol": {
"type": "string"
}
},
"required": [ "protocol" ],
"additionalProperties": false
},
"rcpm_welcome": {
"type": "null"
}
},
"oneOf": [
{ "required": [ "rcpm_version" ] },
{ "required": [ "rcpm_welcome" ] }
],
"additionalProperties": false
}
+106
View File
@@ -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
}
+2
View File
@@ -0,0 +1,2 @@
iccid,kic,kid,kik
8949440000001155306,F09C43EE1A0391665CC9F05AF4E0BD10,01981F4A20999F62AF99988007BAF6CA,8F8AEE5CDCC5D361368BC45673D99195
1 iccid kic kid kik
2 8949440000001155306 F09C43EE1A0391665CC9F05AF4E0BD10 01981F4A20999F62AF99988007BAF6CA 8F8AEE5CDCC5D361368BC45673D99195
@@ -0,0 +1,2 @@
"ICCID","KIC","KID","KIK"
"8949440000001155306","eae46224fa0a4ac1c12cba9d102f1188","3f14b978ddb38c08d832d4e4c2e0639d","9e19db4a5ed5cb8c4f5d96283eab273a"
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIUEv1f0yjVtkr+RNYLItZ33eTJwHMwDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjYwNDI5MTIwOTM0WhcNMzYw
NDI2MTIwOTM0WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBANXdkSyQlDzuo2cJmnBmFiZpc0V9tYBcNkpZd3Ac
R0WljazKKgXDWNmOcSO7891bi+1HZzz+nDfV0mJY776ScGkTqF43Hzpg9eZakMAx
yC24mT4h+uyRcPWZrBwaQhpiQrvZy4MRyuUB+BEgBSmhoDiuXP44kWiuEJHuzpOq
X6Q2dW8RIeQPDGGK6XPZIQLqx+krxkaqphd/vHgT1/yd7Ol5xxMc4x2UuPaVCj0D
OzslFsbb0Zu77ffCtHOVVnzSCzeEGGx1MPQm6hDVW+KUXXTwke1K55fmFZhu0gKO
HYSEjgPj6X8muDb+GvOAQX3fHmS6KvFS4fwWd2InZ3v2f3cCAwEAAaOBkDCBjTAM
BgNVHRMEBTADAQH/MB0GA1UdDgQWBBS6zY4Dd0pJFrvWLmyjn0vDTFqVqzBRBgNV
HSMESjBIgBS6zY4Dd0pJFrvWLmyjn0vDTFqVq6EapBgwFjEUMBIGA1UEAwwLRWFz
eS1SU0EgQ0GCFBL9X9Mo1bZK/kTWCyLWd93kycBzMAsGA1UdDwQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAQEAGJUXlbnVhh+xL+pyTyjwtd8nxhUcHzYZl+OT0bkGY9zT
S3NjHkKBbdnEftuYDYqp0uBuGFQ1WIOKiM3rp4IePKe84lSivZMVh9ObtNalcEQr
sqxBziNOMJM2mh5V2NdxiK2E1gCZ959wOQ8yzM6gGC+wW8w4zwULhv4JimQDjk+G
kAdiGL7+WAxrNWUulvm8khFt2nOlucJg4IAYVt2SI1AFMt/YSXoA4wMwM9QcHGj0
1A069IxX93WVhUpIL1Avwz+KJK0BPY6SM8LYUy6V50Hojp76BB7VD6SxQrSoceUo
6cRNDtCmofOlltfeUJLr1mI4S2tM50bQVsHD92EJBA==
-----END CERTIFICATE-----
@@ -0,0 +1,115 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
42:38:a5:6f:70:53:40:e4:a4:1a:2c:0f:fc:81:13:42
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Easy-RSA CA
Validity
Not Before: Apr 29 12:09:35 2026 GMT
Not After : Aug 1 12:09:35 2028 GMT
Subject: CN=example_ssl_rcpc_rcps_cert
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:ae:0f:e1:ee:fc:f6:db:75:45:c0:f4:49:72:46:
3d:e3:db:0c:c4:34:d2:9e:49:d4:86:4f:19:0d:55:
70:50:81:e4:e6:64:56:a8:58:e8:e6:54:0a:16:bc:
f4:4b:84:cd:1d:b9:2e:ed:62:b6:cd:62:35:8b:81:
18:ab:ff:63:f5:c1:dc:16:3e:a8:dc:ac:11:dd:43:
12:f8:ef:f2:f1:af:84:fd:83:fe:a8:d3:46:7d:77:
e6:ae:95:61:a6:c9:99:6b:40:61:8d:6e:7e:66:1e:
97:77:b0:e8:b7:3d:3a:d5:d7:d3:ee:66:95:62:83:
14:cc:5e:32:ff:9e:bd:f1:06:e6:8d:6a:7c:0a:27:
22:19:b9:06:09:cf:ef:c7:dc:e8:8f:04:4b:83:0d:
cc:8d:b1:c2:cf:ab:40:25:6e:f2:bf:b7:c6:1d:8f:
d2:fc:3d:c8:a1:be:4a:09:b9:91:e3:76:4f:c7:9b:
fc:2f:de:d9:bb:eb:df:d3:d8:8c:72:79:bd:bf:10:
8b:01:e6:0f:7f:bb:f6:75:31:5a:40:ad:df:e1:07:
e6:12:12:b2:d3:99:d0:bd:24:5a:9a:ce:62:4f:da:
fe:0d:df:09:ae:da:04:83:54:e8:cb:68:c0:57:78:
c2:f4:68:42:d7:f4:81:4a:a3:b4:4e:0b:49:95:26:
1d:15
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Subject Key Identifier:
8E:99:9D:C0:70:98:57:16:08:8E:DF:6E:51:78:A6:86:18:FF:06:52
X509v3 Authority Key Identifier:
keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB
DirName:/CN=Easy-RSA CA
serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Key Usage:
Digital Signature, Key Encipherment
X509v3 Subject Alternative Name:
DNS:127.0.0.1, IP Address:127.0.0.1
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
3e:56:20:f9:3b:fa:13:6e:7e:a9:80:a6:15:18:01:82:f1:b8:
4d:1b:f1:ee:da:ed:50:f7:3b:13:01:a5:14:f9:4c:0e:34:57:
dc:e6:d1:7e:02:30:af:3b:fd:c9:ae:18:16:c9:3b:0a:4e:20:
da:cd:e8:cc:05:0c:b3:7d:6f:e5:15:ff:66:59:6b:fe:ff:1a:
ef:ca:b5:3a:1a:ad:dd:f6:19:43:d9:2b:61:18:29:95:b4:0c:
1e:b2:4a:ce:80:d3:1b:59:dc:62:ec:50:21:37:9c:2f:7a:4d:
c2:ac:de:1b:1d:a3:25:e0:e8:33:42:cf:77:31:2a:f2:44:36:
ef:59:89:da:6c:3e:9a:e8:d7:06:39:17:d5:78:82:6d:b6:63:
3f:9a:40:3b:e6:12:58:52:3d:63:4e:85:0b:02:cb:40:d2:8a:
59:8d:8f:ee:4a:c8:97:91:51:a9:2f:1b:15:81:9c:20:dd:94:
08:6f:ac:fa:c6:28:90:6c:17:5a:23:87:9a:5b:e5:c6:2e:f3:
09:66:de:76:1b:60:42:c1:5c:71:88:87:f6:7b:cb:e3:7e:14:
67:c9:a0:15:98:b6:7b:75:40:9a:08:fc:77:39:3a:23:cb:e3:
78:7d:57:f9:a7:66:36:b4:b5:07:de:61:3a:dd:07:58:b3:4f:
41:f6:f4:d9
-----BEGIN CERTIFICATE-----
MIIDhDCCAmygAwIBAgIQQjilb3BTQOSkGiwP/IETQjANBgkqhkiG9w0BAQsFADAW
MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yNjA0MjkxMjA5MzVaFw0yODA4MDEx
MjA5MzVaMCUxIzAhBgNVBAMMGmV4YW1wbGVfc3NsX3JjcGNfcmNwc19jZXJ0MIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArg/h7vz223VFwPRJckY949sM
xDTSnknUhk8ZDVVwUIHk5mRWqFjo5lQKFrz0S4TNHbku7WK2zWI1i4EYq/9j9cHc
Fj6o3KwR3UMS+O/y8a+E/YP+qNNGfXfmrpVhpsmZa0BhjW5+Zh6Xd7Dotz061dfT
7maVYoMUzF4y/5698QbmjWp8CiciGbkGCc/vx9zojwRLgw3MjbHCz6tAJW7yv7fG
HY/S/D3Iob5KCbmR43ZPx5v8L97Zu+vf09iMcnm9vxCLAeYPf7v2dTFaQK3f4Qfm
EhKy05nQvSRams5iT9r+Dd8JrtoEg1Toy2jAV3jC9GhC1/SBSqO0TgtJlSYdFQID
AQABo4G+MIG7MAkGA1UdEwQCMAAwHQYDVR0OBBYEFI6ZncBwmFcWCI7fblF4poYY
/wZSMFEGA1UdIwRKMEiAFLrNjgN3SkkWu9YubKOfS8NMWpWroRqkGDAWMRQwEgYD
VQQDDAtFYXN5LVJTQSBDQYIUEv1f0yjVtkr+RNYLItZ33eTJwHMwEwYDVR0lBAww
CgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMBoGA1UdEQQTMBGCCTEyNy4wLjAuMYcE
fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAPlYg+Tv6E25+qYCmFRgBgvG4TRvx7trt
UPc7EwGlFPlMDjRX3ObRfgIwrzv9ya4YFsk7Ck4g2s3ozAUMs31v5RX/Zllr/v8a
78q1Ohqt3fYZQ9krYRgplbQMHrJKzoDTG1ncYuxQITecL3pNwqzeGx2jJeDoM0LP
dzEq8kQ271mJ2mw+mujXBjkX1XiCbbZjP5pAO+YSWFI9Y06FCwLLQNKKWY2P7krI
l5FRqS8bFYGcIN2UCG+s+sYokGwXWiOHmlvlxi7zCWbedhtgQsFccYiH9nvL434U
Z8mgFZi2e3VAmgj8dzk6I8vjeH1X+admNrS1B95hOt0HWLNPQfb02Q==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuD+Hu/PbbdUXA
9ElyRj3j2wzENNKeSdSGTxkNVXBQgeTmZFaoWOjmVAoWvPRLhM0duS7tYrbNYjWL
gRir/2P1wdwWPqjcrBHdQxL47/Lxr4T9g/6o00Z9d+aulWGmyZlrQGGNbn5mHpd3
sOi3PTrV19PuZpVigxTMXjL/nr3xBuaNanwKJyIZuQYJz+/H3OiPBEuDDcyNscLP
q0AlbvK/t8Ydj9L8PcihvkoJuZHjdk/Hm/wv3tm769/T2Ixyeb2/EIsB5g9/u/Z1
MVpArd/hB+YSErLTmdC9JFqazmJP2v4N3wmu2gSDVOjLaMBXeML0aELX9IFKo7RO
C0mVJh0VAgMBAAECggEAGBhmQhdeE+Cu1Ihsn2dWW3PAF2wpiNR3GVWbRfOBHf/x
QCx9K4ZNTU8ua1niZo7edyIiuyVaYWGaQHLRR8QNoiBhN2oapZujSHInzvKmeqr9
ubt7NgMzQ5ykwB+5OiW3uXda2cGFOV08QgspF+6ftakQMzUbslyrdSQIIscmi5Ya
uTDvE6+lBFinxy3RHFKVCZ3UrsDwfHR4eTUmgCHRB27joB7DFXL32amv0M8HjoGz
EZKGJgTwmRf9U4z4D4wCnOfVAPlsuthKUqMuTlBg0ZEstMZrzlP4suT2ieku0Usv
0XbJ38VozPYYFdR7nApVVvrJgHzI9cpoUbGto4BLOQKBgQDbXjFVLffOec8hv9dN
2VGZQmK61S9OrbvTnEOlxJd+kRid71X1pV5TuPKJJQtUJXf429bQOs40YbLeOmJt
BiRSR5yIBH7hDDC/c0ynqunstwDlgz+QX2Oh2B4alvVaWy0rZYF6NpBiI0+R5r5V
C2fHRS4LLPoflg83+CMubyLS6QKBgQDLIOXxlp1JQTzXhJkrkytLkafmEHAafovt
wbRD50/s+dl16BRX12sK0gXj2vwu0FleUD6Z7afDfspmvQdg3fyDxYw9Q+vw5LYQ
7BvoVU99o1m468yXwX/v36peCt4nOpwkJZKJfjgxjnMJByyeSUgL9uW4K+0D0LBV
a5Iv7QklTQKBgH30BkVPIHKIE/rfyIJlXemuaTu2/fOh4y9sEJdUWluMeeLssaFa
ct+FWJSQFYIaBVl4+E0VBqKi2e2o/ix1E1O+1ExwsF0M/8xdKk024BtPNA+TnWKK
so0Rpq9Dr9pScYvyOzZtr9b5SU2PfAcehlavDPHTwEV0hoZvTdvyab9JAoGADMBJ
7vp3cSvJN/Y470VTyHCiS4zonKEpA4nPWRviJowgnIgvDryVGZ7Jg94xSncFxSfg
ZiVHDLye1Ag1uFz3BwaVoRrsarjQvQs1TUZdsRNaBIO42iXpdBNkTHb+LxQ8zQAW
zM7BlErO6dgrctxCy416Ki+Ht1+YUiRojt2gX1kCgYBqytUy+XkPi5j3Ga29xcvP
WI3Uc8RI2GmoAmrw5QFiSG6lNXAzfo2ZNQbFnxgxeMOG9fV9yzBdIjXWNWr0E/KH
Fsb65R8iIrXQB9BZjuQqjz9nDm7eZZUBNGGbQ4DgSepnp194gXC5DoAElzuwOXbE
pY/kM1KwlpUR3J3LeF3i+Q==
-----END PRIVATE KEY-----
@@ -0,0 +1,115 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
e7:09:ab:70:b5:dc:1f:11:d9:2a:23:04:39:87:34:f3
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Easy-RSA CA
Validity
Not Before: Apr 29 12:09:35 2026 GMT
Not After : Aug 1 12:09:35 2028 GMT
Subject: CN=example_ssl_rcpm_rcps_cert
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:cc:79:9b:d3:f3:1f:41:9f:00:48:cd:47:0b:ae:
b9:1c:4e:3e:55:e2:4e:5f:a8:cc:13:d5:dd:bd:f0:
01:4c:19:ae:e3:a9:09:06:89:92:49:f7:bb:90:28:
fb:8c:22:69:b5:f5:a0:50:3d:97:0f:1e:1d:b1:a8:
57:9b:d7:e2:0d:99:67:7f:02:82:0c:9c:8e:dd:13:
03:28:93:b5:cb:7e:b5:78:06:10:bf:7b:55:c3:f7:
10:8b:20:4a:1c:f9:f1:b2:fa:f1:c7:44:9d:0a:ce:
ef:8d:f9:e8:ff:d1:c1:69:ec:8e:5f:11:cc:c9:98:
d5:1c:33:e2:5b:7a:4d:34:dc:76:c3:cd:db:4c:93:
d1:08:78:6f:3c:9a:ee:74:39:1e:cd:65:1e:c9:35:
cc:3b:2b:9e:d7:49:10:8e:58:85:b0:10:5b:90:1e:
f1:5e:d5:92:04:93:f9:33:c6:9d:77:63:d1:33:46:
5b:98:ff:9a:a8:f5:df:f7:84:21:e2:88:28:7a:a4:
c6:0d:9f:25:7e:0d:73:5b:d5:53:4a:90:79:94:37:
14:f3:c8:75:76:d4:1c:32:51:bf:58:16:74:d5:8d:
18:b6:53:f4:ab:cb:91:a8:8c:a3:ca:3c:5c:35:b6:
5f:62:57:37:5a:75:28:b7:4d:26:aa:ea:50:da:a4:
1c:55
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Subject Key Identifier:
47:92:B5:81:8B:5C:14:98:B3:83:B6:EB:06:9F:43:F3:3A:7E:ED:24
X509v3 Authority Key Identifier:
keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB
DirName:/CN=Easy-RSA CA
serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Key Usage:
Digital Signature, Key Encipherment
X509v3 Subject Alternative Name:
DNS:127.0.0.1, IP Address:127.0.0.1
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
6d:31:e6:29:d2:3b:a8:90:5c:4b:ac:61:15:95:5d:70:66:a5:
77:9d:88:47:49:73:75:be:70:69:d8:2f:62:82:5e:83:86:3b:
a8:48:3f:f1:5f:22:ae:81:23:64:c4:f2:2b:dd:4d:be:e5:6a:
26:a5:ea:c7:ba:1b:3e:6a:34:03:5a:f1:49:28:5f:56:4a:a6:
0e:1b:7a:07:48:76:95:b6:4b:f5:3f:b9:67:2e:e0:33:06:80:
d4:d6:01:a5:76:01:c0:a5:18:e5:38:8b:52:73:6e:6d:45:50:
b7:9a:ab:86:5d:e3:65:b4:b8:c7:ee:b2:dc:bf:e3:d5:bb:e4:
91:eb:f5:0c:38:22:5e:37:54:9e:ba:96:25:10:04:18:23:f7:
ae:73:4d:d0:aa:03:81:b4:89:36:97:15:da:1a:60:a0:98:5f:
03:f8:1b:22:83:57:41:4b:12:28:7d:8d:ea:88:74:24:28:5c:
53:41:89:5e:9a:da:fd:7b:bf:60:dc:de:9b:49:ce:5c:a3:b2:
01:7d:1d:cb:28:8c:ba:f4:7b:5d:2b:cb:15:5b:2a:97:1a:d1:
f9:e7:12:e3:43:b9:f4:2a:88:dd:6d:b6:a0:72:d3:bd:63:23:
e9:d7:f0:ac:b5:6d:0d:f2:d9:8b:2c:c4:35:5b:4d:83:dc:e8:
7d:0b:3d:a3
-----BEGIN CERTIFICATE-----
MIIDhTCCAm2gAwIBAgIRAOcJq3C13B8R2SojBDmHNPMwDQYJKoZIhvcNAQELBQAw
FjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjYwNDI5MTIwOTM1WhcNMjgwODAx
MTIwOTM1WjAlMSMwIQYDVQQDDBpleGFtcGxlX3NzbF9yY3BtX3JjcHNfY2VydDCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMx5m9PzH0GfAEjNRwuuuRxO
PlXiTl+ozBPV3b3wAUwZruOpCQaJkkn3u5Ao+4wiabX1oFA9lw8eHbGoV5vX4g2Z
Z38Cggycjt0TAyiTtct+tXgGEL97VcP3EIsgShz58bL68cdEnQrO74356P/RwWns
jl8RzMmY1Rwz4lt6TTTcdsPN20yT0Qh4bzya7nQ5Hs1lHsk1zDsrntdJEI5YhbAQ
W5Ae8V7VkgST+TPGnXdj0TNGW5j/mqj13/eEIeKIKHqkxg2fJX4Nc1vVU0qQeZQ3
FPPIdXbUHDJRv1gWdNWNGLZT9KvLkaiMo8o8XDW2X2JXN1p1KLdNJqrqUNqkHFUC
AwEAAaOBvjCBuzAJBgNVHRMEAjAAMB0GA1UdDgQWBBRHkrWBi1wUmLODtusGn0Pz
On7tJDBRBgNVHSMESjBIgBS6zY4Dd0pJFrvWLmyjn0vDTFqVq6EapBgwFjEUMBIG
A1UEAwwLRWFzeS1SU0EgQ0GCFBL9X9Mo1bZK/kTWCyLWd93kycBzMBMGA1UdJQQM
MAoGCCsGAQUFBwMBMAsGA1UdDwQEAwIFoDAaBgNVHREEEzARggkxMjcuMC4wLjGH
BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAG0x5inSO6iQXEusYRWVXXBmpXediEdJ
c3W+cGnYL2KCXoOGO6hIP/FfIq6BI2TE8ivdTb7laial6se6Gz5qNANa8UkoX1ZK
pg4begdIdpW2S/U/uWcu4DMGgNTWAaV2AcClGOU4i1Jzbm1FULeaq4Zd42W0uMfu
sty/49W75JHr9Qw4Il43VJ66liUQBBgj965zTdCqA4G0iTaXFdoaYKCYXwP4GyKD
V0FLEih9jeqIdCQoXFNBiV6a2v17v2Dc3ptJzlyjsgF9HcsojLr0e10ryxVbKpca
0fnnEuNDufQqiN1ttqBy071jI+nX8Ky1bQ3y2YssxDVbTYPc6H0LPaM=
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMeZvT8x9BnwBI
zUcLrrkcTj5V4k5fqMwT1d298AFMGa7jqQkGiZJJ97uQKPuMImm19aBQPZcPHh2x
qFeb1+INmWd/AoIMnI7dEwMok7XLfrV4BhC/e1XD9xCLIEoc+fGy+vHHRJ0Kzu+N
+ej/0cFp7I5fEczJmNUcM+Jbek003HbDzdtMk9EIeG88mu50OR7NZR7JNcw7K57X
SRCOWIWwEFuQHvFe1ZIEk/kzxp13Y9EzRluY/5qo9d/3hCHiiCh6pMYNnyV+DXNb
1VNKkHmUNxTzyHV21BwyUb9YFnTVjRi2U/Sry5GojKPKPFw1tl9iVzdadSi3TSaq
6lDapBxVAgMBAAECggEADJFN6K9OWhYX1PcEWUgOLxqdCLd95Iccsfxot7ekcMUP
A4WnHRyACLqor9c2V3o2//IpU2fnB2IXu6ISmRd3WKl3hm4vnZmoIJeTpQm9Iv/g
+fqkyrbIgktcHDJUySal+n+jiYFNW2B1h1xXUT/scMz+FthNJg1Azfi0vorMFjCk
SBOSo7BQ2hiQ83FneVJU1TsxD4S4IBLx9fF6AW05norRmvm17Ip2pKk4QYzKiOI3
NIoSwbOgU0Vp1X3MMlilM5ZZdN3a9lI6lfBZE682WOxBmH67mKMQnR8aC+nwynQ+
45pUQIn8Fjx2hQHehbD7ZNw9Nob9AyGWqWGKFV74IQKBgQDox/j7dCHNm1mz1QRm
Q2YyN4OGXJI97t9Gd8UdNCv5bAkdx2cR2lmP3tRc6iAzrfNIpPCGaXo1vwJx477X
wO95W2b4hfm3j6v0cqRbsFzzVIHZUB77pXfAJfZeICpoqu0vxn5nb+yPgzgmToLX
pbIDdqWafzzrDLTmLCfwfKDI9QKBgQDg3tgpAXo8WCL4phNuW44XQ172lPTijpn+
wj1Z0rBrS806gL/+QvZZLS1WCym/QBV7TgGlxIJbmAfghcGyin3NliskSHAiccxG
/9eCSQes8czfsVj6qmBMwyff5r+wmk662qV0u07UHmuykYk3Dgs/zYdwq2SsTlL4
Y9eRjutp4QKBgQCONg0wYcR8/hmROeRULXzz1OJvZYKaf6K8RFOSAduTp6LyJG4d
hA4PTQzkLsy5hd4JVWr0UuAskaMGvSJMYTxsIaEI16C1ufpNfvRWZ6qBpfEmOEKV
boN4Sjj3TCNcioAZHeT/gGs/SeU10eUxpbLZVtTZTD6FQuAJdpR34UvBOQKBgFNM
mXxPLM2vxHyhYK9PwQoDDel/8lr+gjMqFvnwHyQP911FllmEyqbsIlAuYG+VOJ/t
nJSgf72YSsq0IbWWsdV3XFHbd5Z62zYtzdJYZTx+cesnUhPBC11EKcA6RSYRczqq
hgIA5MmU30ZNvSukyyv+Yb6t7uQZO4kByzgDXldhAoGBALgRkAHxgbKUXp5XyCDJ
e8dwVx0g9tfDM/DEZtU/Si5oUaunBaPV/Byov7OXOT02V8JLnA5ChUYgwUFI030x
QL/3eK12Qh5Gb9VabvYCicDRk4GzmqZU9Wcvm1zgbUr5jY8Lou44nFjol/Y1m70n
51WZbVkkWmBZO5m3NqN66SkJ
-----END PRIVATE KEY-----
@@ -0,0 +1,115 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
28:96:2e:a1:40:e0:7e:f1:fb:63:1a:f4:53:6f:ce:fb
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Easy-RSA CA
Validity
Not Before: Apr 29 12:09:35 2026 GMT
Not After : Aug 1 12:09:35 2028 GMT
Subject: CN=example_ssl_rcps_rcpm_cert
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:ae:11:46:ef:d1:81:34:dd:23:5d:54:40:f3:9c:
85:35:95:a6:91:57:92:5c:bf:eb:40:34:69:eb:db:
c0:86:3c:7b:ff:9c:d7:ba:0e:41:57:84:15:cd:94:
f1:48:63:50:9c:34:97:ee:be:be:b0:27:d8:fd:cd:
8a:cf:85:ff:08:1f:07:d8:28:96:0e:e4:2d:d0:8b:
df:a8:fa:41:47:a0:a2:80:2e:2e:58:01:cc:6f:43:
5c:c2:fb:84:a7:ff:9e:97:bb:b3:a3:1f:63:64:73:
8d:73:dd:f4:7e:96:d7:6b:b3:cb:e2:35:59:55:e0:
e7:e3:c0:41:f8:b6:0f:c5:46:4c:cd:0e:91:80:ef:
e3:43:f0:72:26:12:10:be:83:a2:db:23:2d:b4:b1:
07:5a:b1:b3:10:9c:09:69:98:42:79:81:77:5e:22:
e4:71:47:70:27:15:2c:a7:13:c2:6d:44:59:b4:73:
c9:bb:27:7f:d6:e8:3d:85:bb:36:f6:cb:71:36:11:
b1:99:1a:1d:1a:15:dd:cd:65:7f:cd:cc:10:00:49:
ed:07:2d:7b:15:88:be:73:ba:1d:15:69:bc:d3:02:
55:ea:dc:2c:3f:0b:cd:18:57:59:7a:e3:09:b2:89:
cd:d6:e7:f6:95:c4:2e:8a:53:2b:a8:96:82:94:53:
00:77
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Subject Key Identifier:
60:BD:48:06:68:15:D4:DC:ED:EE:E4:C7:B1:9F:C4:93:6D:50:3A:77
X509v3 Authority Key Identifier:
keyid:BA:CD:8E:03:77:4A:49:16:BB:D6:2E:6C:A3:9F:4B:C3:4C:5A:95:AB
DirName:/CN=Easy-RSA CA
serial:12:FD:5F:D3:28:D5:B6:4A:FE:44:D6:0B:22:D6:77:DD:E4:C9:C0:73
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Key Usage:
Digital Signature, Key Encipherment
X509v3 Subject Alternative Name:
DNS:127.0.0.1, IP Address:127.0.0.1
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
c5:35:61:58:23:e2:69:da:6c:d5:41:ab:a8:70:f4:dd:cc:a0:
a3:3d:84:89:93:b6:7f:69:7d:10:35:9d:c5:d1:0d:db:d2:d7:
36:af:d4:54:30:14:a7:5d:31:ca:5c:13:92:d5:60:50:f8:56:
4a:cb:16:b1:b3:b1:03:bf:96:53:77:1f:4a:0f:9c:29:2b:bf:
a4:e0:da:6f:ad:13:c7:2d:8e:18:c4:72:50:17:ed:1f:36:51:
7a:12:9f:fc:a6:d6:c8:55:e0:db:ea:16:d6:22:0d:a2:cb:eb:
b2:ba:07:92:2f:db:33:d6:a2:0c:ec:89:29:f1:96:40:e5:0b:
e6:1f:08:50:d6:29:87:a8:20:b2:e2:17:50:25:ff:53:36:ee:
7f:ce:e6:1d:ed:b3:16:61:18:42:a9:17:9e:a6:86:0d:a5:fc:
f9:42:c8:50:48:74:72:35:eb:8c:ff:4d:e8:98:88:a0:b4:b3:
d0:82:b3:2f:ea:19:d7:d5:ac:47:35:96:24:37:34:0c:7a:a2:
e0:4d:99:a7:55:61:85:1e:7e:6a:23:77:f5:07:13:e6:50:5c:
65:00:13:f6:b5:4b:5b:8c:11:c3:5d:af:ba:41:e9:84:1d:f1:
a4:70:16:28:c2:be:6e:d8:67:38:c5:a0:ba:8a:64:6f:27:ce:
63:a0:92:9b
-----BEGIN CERTIFICATE-----
MIIDhDCCAmygAwIBAgIQKJYuoUDgfvH7Yxr0U2/O+zANBgkqhkiG9w0BAQsFADAW
MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yNjA0MjkxMjA5MzVaFw0yODA4MDEx
MjA5MzVaMCUxIzAhBgNVBAMMGmV4YW1wbGVfc3NsX3JjcHNfcmNwbV9jZXJ0MIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhFG79GBNN0jXVRA85yFNZWm
kVeSXL/rQDRp69vAhjx7/5zXug5BV4QVzZTxSGNQnDSX7r6+sCfY/c2Kz4X/CB8H
2CiWDuQt0IvfqPpBR6CigC4uWAHMb0NcwvuEp/+el7uzox9jZHONc930fpbXa7PL
4jVZVeDn48BB+LYPxUZMzQ6RgO/jQ/ByJhIQvoOi2yMttLEHWrGzEJwJaZhCeYF3
XiLkcUdwJxUspxPCbURZtHPJuyd/1ug9hbs29stxNhGxmRodGhXdzWV/zcwQAEnt
By17FYi+c7odFWm80wJV6twsPwvNGFdZeuMJsonN1uf2lcQuilMrqJaClFMAdwID
AQABo4G+MIG7MAkGA1UdEwQCMAAwHQYDVR0OBBYEFGC9SAZoFdTc7e7kx7GfxJNt
UDp3MFEGA1UdIwRKMEiAFLrNjgN3SkkWu9YubKOfS8NMWpWroRqkGDAWMRQwEgYD
VQQDDAtFYXN5LVJTQSBDQYIUEv1f0yjVtkr+RNYLItZ33eTJwHMwEwYDVR0lBAww
CgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMBoGA1UdEQQTMBGCCTEyNy4wLjAuMYcE
fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAxTVhWCPiadps1UGrqHD03cygoz2EiZO2
f2l9EDWdxdEN29LXNq/UVDAUp10xylwTktVgUPhWSssWsbOxA7+WU3cfSg+cKSu/
pODab60Txy2OGMRyUBftHzZRehKf/KbWyFXg2+oW1iINosvrsroHki/bM9aiDOyJ
KfGWQOUL5h8IUNYph6ggsuIXUCX/Uzbuf87mHe2zFmEYQqkXnqaGDaX8+ULIUEh0
cjXrjP9N6JiIoLSz0IKzL+oZ19WsRzWWJDc0DHqi4E2Zp1VhhR5+aiN39QcT5lBc
ZQAT9rVLW4wRw12vukHphB3xpHAWKMK+bthnOMWguopkbyfOY6CSmw==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuEUbv0YE03SNd
VEDznIU1laaRV5Jcv+tANGnr28CGPHv/nNe6DkFXhBXNlPFIY1CcNJfuvr6wJ9j9
zYrPhf8IHwfYKJYO5C3Qi9+o+kFHoKKALi5YAcxvQ1zC+4Sn/56Xu7OjH2Nkc41z
3fR+ltdrs8viNVlV4OfjwEH4tg/FRkzNDpGA7+ND8HImEhC+g6LbIy20sQdasbMQ
nAlpmEJ5gXdeIuRxR3AnFSynE8JtRFm0c8m7J3/W6D2Fuzb2y3E2EbGZGh0aFd3N
ZX/NzBAASe0HLXsViL5zuh0VabzTAlXq3Cw/C80YV1l64wmyic3W5/aVxC6KUyuo
loKUUwB3AgMBAAECggEABGfLiSZJmYeUmDgZrLtkDlx16sJx9zGkR+u2V+cn6D3U
+uiCoo2EedfjSrYKT/AI35Xf19sGrc6ptJgPJfwY3aEWFxJv5HtB7ZVHWS98QiPT
+QqHgb1fPzGlQgoQ7Bo8GVBW1joPz0Bdbsv0ntTn1CyowauNUe8Z71mzpxJ0iQ7u
Z8LNoD7INEAZDBjHVov6pLGDHS9KFxGy20WG549mE37I4QxyxetJEgmuyhJkZOQ4
noEWiCMQjGsSg4YuSc1GS1jAVf3p2g3/TiheD/31r1jleY/T5s2qYC6MJ4vY+7yA
5sl7m8A47i0lHSKBdR3nWz+vXEsxB0nXK3Sulyt9AQKBgQDYRZ1nEe32CCZcWn/7
nG+9e6XNOe9E25skuvY/uEpikt92kdnyZOgHFfwM9Nv+w2IWGLZ8MmDDJ6/4hGvj
fJjcOUb3d/SJiivPvdRC9GYMrDUZe5AJ3p6fPqi4IeZOw7bMTcVB6aHAr9KoO//J
2t0WNvwzkOyl6KlhaQEIDK8bOQKBgQDOCvWvUEg8nHaKcmN0uQaKuPvCorhnyqUT
VFqLlSrYC79ffHCwp2y8nkFUnxpeHXENrtHtcrdnKBNkgGEn2l4xs63CjhKrBuIG
bDjrtp3vKlHRhQjX6HkrEEuUk52wYzuX7CfU8nTZnrtV74MocOEewJVW+84Rtwiw
vIUcgfgJLwKBgAbgj9TLOSntsGqXZiJ2Iwd/exI/mWAzK4fLejEkhxkDWp/Gm4ud
sdMn28/9qVE8nU3ek073uyP5ixr3+wZM2/+EwsDzy47kGeiNPMa0Rtp4T2f0CeyG
a7zcnTjduxkeGB3/CxrBdydNcAFxhvzAPO+L6BErtpq//0Ldt+6tmJPhAoGAFxEh
Cjx5qdd2ae9+dO3V7qfg/5xJ+sy0CGL0NBZCEqfWB/GdiBlmUgOBmuCpCgpPwtFk
jSm/oJva9/BrcBPBYd0Uweg37M+7dC6ffLwYGFNrj4JOSCWtkwWjAII6MCob3NlC
aFOwg0CDBo7m5xskCNZUocVU/6S3I1onqNZgF18CgYAczBf1NS4VPZvebrutTBEH
zyUX3XU9mR+dy0ncCGNVS8zYMtz7cweZInzNB2cTOfisIHzzdOnazm7D9uzPsREi
pKgaL+ErWYDlGiDTxMtGSRPTWGocYBYdU6y/0bobhZb0qyvyRhpGvPK0ReMUuvqu
FkNgoQ1lo0n6vawvxWW8Mw==
-----END PRIVATE KEY-----
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
EASYRSA=/usr/share/easy-rsa/easyrsa
CA_NAME="example_ssl_rcp_ca_cert"
export EASYRSA_PASSIN=pass:test
export EASYRSA_PASSOUT=pass:test
echo "Cleaning up..."
rm -rf ./ca
rm -rf ./*.pem
rm -rf ./*.key
rm -rf ./*.crt
echo "Creating CA cert..."
mkdir -p ./ca
cd ./ca
$EASYRSA init-pki
cp ../vars ./pki/
$EASYRSA --batch build-ca
cp ./pki/ca.crt ../$CA_NAME.crt
echo "Creating server certs..."
# Secures connection between RCP-Client and RCP-Server:
$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcpc_rcps_cert nopass
# Secures connection between RCP-Module and RCP-Server (module description):
$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcpm_rcps_cert nopass
# Secures connection between RCP-Server and RCP-Module (command execution):
$EASYRSA --batch --subject-alt-name="DNS:127.0.0.1,IP:127.0.0.1" build-server-full example_ssl_rcps_rcpm_cert nopass
echo "Collecting server certs..."
cp ./pki/issued/* ../
cp ./pki/private/* ../
cd ..
rm ./ca.key
echo "Merging server certs..."
for CRT in ./*.crt; do
CRT_NAME=`basename ${CRT%.*}`
if [ -f $CRT_NAME.key ]; then
cat $CRT_NAME.crt $CRT_NAME.key > $CRT_NAME.pem
rm $CRT_NAME.key
rm $CRT_NAME.crt
fi
done
echo "Finalizing..."
rm -rf ./ca
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
. ./params.cfg
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/csv-encrypt-columns.py \
--csv-column-key kic:$CSV_COLUMN_KEY \
--csv-column-key kid:$CSV_COLUMN_KEY \
--csv-column-key kik:$CSV_COLUMN_KEY \
card_data.csv
+36
View File
@@ -0,0 +1,36 @@
# Verbosity switch passed to all components (comment-out to disable verbose mode)
#VERBOSE="--verbose"
# PYSIM_DIR passed to all components
PYSIM_DIR=../../../ # Points to the psyim top directory
# CSV column key to decrypt KIC, KID and KIK in csv_data.csv.encr
# (use encrypt_card_data.sh to regenerate csv_data.csv.encr from csv_data.csv)
CSV_COLUMN_KEY="00112233445566778899AABBCCDDEEFF"
# PCSC reader that the RCP Client shall use
PCSC_READER=0
# CA of the certificates used in this example
CERT_DIR="./certs"
CA_CERT="$CERT_DIR/example_ssl_rcp_ca_cert.crt"
# Network interface where RCP Clients connect
RCPC_SERVER_PORT=8000
RCPC_SERVER_ADDR="127.0.0.1"
RCPC_SERVER_CERT="$CERT_DIR/example_ssl_rcpc_rcps_cert.pem"
RCPC_SERVER_URI="wss://$RCPC_SERVER_ADDR:$RCPC_SERVER_PORT"
# Network interface where RCP Modules connect
RCPM_SERVER_PORT=8010
RCPM_SERVER_ADDR="127.0.0.1"
RCPM_SERVER_CERT="$CERT_DIR/example_ssl_rcpm_rcps_cert.pem"
RCPM_SERVER_URI="wss://$RCPM_SERVER_ADDR:$RCPM_SERVER_PORT"
# Network interface where the (example) RCP Module binds its Command Server to.
# The command server is used by the RCP Server to run the command requested
# by the user. Each module needs a dedicated port. The address and port is
# automatically forwarded to the RCP Server.
RCPM_CMD_SERVER_PORT=8020
RCPM_CMD_SERVER_ADDR="127.0.0.1"
RCPM_CMD_SERVER_CERT="$CERT_DIR/example_ssl_rcps_rcpm_cert.pem"
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
# (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from pathlib import Path
from pySim.log import PySimLogger
from argparse import Namespace
from pySim.global_platform import GpCardKeyset, SCP02, ADF_SD
from Cryptodome.Random import get_random_bytes
from osmocom.utils import h2b, b2h
from rcp_module_utils import rcpm_setup_argparse, rcpm_run_module, RcpModule, RcpModuleHdlr
log = PySimLogger.get(Path(__file__).stem)
option_parser = rcpm_setup_argparse("Example Module")
class ExmpleModule(RcpModule):
def __init__(self, *args, **kwargs):
log.info("rcpm_run_module was called with the following additional arguments:")
log.info("%s, %s", str(args), str(kwargs))
name = 'rcp_module'
cmd_descr = [{'name' : 'reset',
'help': 'reset the card',
'args' : []},
{'name' : 'read_binary',
'help': 'read binary data from a transparent file.',
'args' : [{ 'name' : '--fid',
'spec' : {'required' : True,
'help' : 'File identifier to of the file to read',
'action' : 'append',
'pytype' : 'str'},
}
]},
{'name' : 'read_record',
'help': 'read binary data from a transparent file.',
'args' : [{ 'name' : '--fid',
'spec' : {'required' : True,
'help' : 'File identifier to of the file to read',
'action' : 'append',
'pytype' : 'str'},
},
{ 'name' : '--record',
'spec' : {'required' : True,
'help' : 'File record to read',
'default' : 1,
'pytype' : 'int'},
}
]},
{'name' : 'unlock_aram',
'help': 'unlock a locked ARA-M applet on a sysmoISIM-SJA5',
'args' : [],
'get_keys' : {'uicc' : ['KIC', 'KID', 'KIK']}}
]
suitable_for = [{'atr' : '3b9f96801f878031e073fe211b674a357530350265f8'}]
def cmd_reset(self, hdlr: RcpModuleHdlr) -> int:
hdlr.print("resetting UICC/eUICC ...")
hdlr.card._scc.reset_card()
hdlr.print("ATR is: %s" % hdlr.card._scc.get_atr())
return 0
def cmd_read_binary(self, hdlr: RcpModuleHdlr) -> int:
fid = hdlr.cmd_args.fid
hdlr.print("reading transparent file: %s ..." % fid)
(res, _) = hdlr.card._scc.read_binary(fid)
hdlr.print("file content is: %s" % res)
return 0
def cmd_read_record(self, hdlr: RcpModuleHdlr) -> int:
fid = hdlr.cmd_args.fid
record = hdlr.cmd_args.record
hdlr.print("reading linear-fixed file: %s ..." % fid)
(res, _) = hdlr.card._scc.read_record(fid, record)
hdlr.print("file content is: %s" % res)
return 0
def cmd_unlock_aram(self, hdlr: RcpModuleHdlr) -> int:
# Select ADF.ISD
hdlr.print("Selecting ADF.ISD ...")
hdlr.lchan.scc.send_apdu_checksw("00a4040408a00000000300000000")
# Establish secure channel
hdlr.print("Establishing secure channel ...")
key_ver = 112
key_enc = hdlr.keys_uicc['KIC']
key_mac = hdlr.keys_uicc['KID']
key_dek = hdlr.keys_uicc['KIK']
security_level = 3
host_challenge_len = 8
host_challenge = get_random_bytes(host_challenge_len)
kset = GpCardKeyset(key_ver, h2b(key_enc), h2b(key_mac), h2b(key_dek))
scp = SCP02(card_keys=kset)
ADF_SD.establish_scp(hdlr.lchan.scc, scp, host_challenge, security_level)
# To prove that it works, we need to do something that actually requires to be authenticated
# via a secure channel. In this example we will send an unlock command to the ARA-M applet
# found on any sysmoISIM-SJA5 card. (see also: https://gitea.osmocom.org/sim-card/aram-applet)
hdlr.print("Unlocking ARA-M applet ...")
ara_m_aid = "a00000015141434c00"
ADF_SD.install(hdlr.lchan.scc, 0x20, 0x00, "0000%02x%s000000" % (len(ara_m_aid) // 2, ara_m_aid))
ADF_SD.store_data(hdlr.lchan.scc, h2b("A2"), structure = 'ber_tlv')
# Release the secure channel
hdlr.print("Done, releasing secure channel ...")
ADF_SD.release_scp(hdlr.lchan.scc)
return 0
if __name__ == '__main__':
opts = option_parser.parse_args()
rcpm_run_module(opts, ExmpleModule,
"arg1", "arg2", "arg3",
kwarg1="kwarg1", kwarg2="kwarg2", kwarg3="kwarg3")
+16
View File
@@ -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)
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
echo "basic help"
echo "===================================================================================="
./run_rcp_client_help.sh
echo "===================================================================================="
echo ""
echo ""
echo "help for which commands are available"
echo "===================================================================================="
./run_rcp_client_help_cmd.sh
echo "===================================================================================="
echo ""
echo ""
echo "help for specific commands"
echo "===================================================================================="
./run_rcp_client_help_cmd_specific.sh
echo "===================================================================================="
echo ""
echo ""
echo "run specific RCP commands"
echo "===================================================================================="
./run_rcp_client_cmd.sh
echo "===================================================================================="
echo ""
echo ""
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
. ./params.cfg
set -x
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI\
--ca-cert $CA_CERT \
-p $PCSC_READER \
rcp_module_reset
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI \
--ca-cert $CA_CERT \
-p $PCSC_READER \
rcp_module_read_binary --fid 3f00 --fid 2fe2
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI \
--ca-cert $CA_CERT \
-p $PCSC_READER \
rcp_module_read_record --fid 3f00 --fid 2f00 --record 1
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI \
--ca-cert $CA_CERT \
-p $PCSC_READER \
rcp_module_unlock_aram
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
. ./params.cfg
set -x
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
-h
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
. ./params.cfg
set -x
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI \
--ca-cert $CA_CERT \
-p $PCSC_READER \
-h
@@ -0,0 +1,28 @@
#!/bin/bash
. ./params.cfg
set -x
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI \
--ca-cert $CA_CERT \
-p $PCSC_READER \
rcp_module_reset --help
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI \
--ca-cert $CA_CERT \
-p $PCSC_READER \
rcp_module_read_binary --help
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI \
--ca-cert $CA_CERT \
-p $PCSC_READER \
rcp_module_read_record --help
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_client.py $VERBOSE \
--uri $RCPC_SERVER_URI \
--ca-cert $CA_CERT \
-p $PCSC_READER \
rcp_module_unlock_aram --help
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
. ./params.cfg
set -x
PYTHONPATH=$PYSIM_DIR:$PYSIM_DIR/contrib/rcp ./rcp_module.py $VERBOSE \
--uri $RCPM_SERVER_URI \
--rcps-ca-cert $CA_CERT \
--rcpm-cmd-server-addr $RCPM_CMD_SERVER_ADDR \
--rcpm-cmd-server-port $RCPM_CMD_SERVER_PORT \
--rcpm-cmd-server-cert $RCPM_CMD_SERVER_CERT \
--column-key kic:$CSV_COLUMN_KEY \
--column-key kid:$CSV_COLUMN_KEY \
--column-key kik:$CSV_COLUMN_KEY
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
. ./params.cfg
set -x
PYTHONPATH=$PYSIM_DIR $PYSIM_DIR/contrib/rcp/rcp_server.py $VERBOSE \
--rcpc-server-addr $RCPC_SERVER_ADDR \
--rcpc-server-port $RCPC_SERVER_PORT \
--rcpc-server-cert $RCPC_SERVER_CERT \
--rcpm-server-addr $RCPM_SERVER_ADDR \
--rcpm-server-port $RCPM_SERVER_PORT \
--rcpm-server-cert $RCPM_SERVER_CERT \
--rcpm-module-ca-cert $CA_CERT \
--csv ./card_data.csv.encr
+3
View File
@@ -14,6 +14,8 @@ import os
import sys
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath('.')) # for local extensions (pysim_fs_sphinx, ...)
sys.path.insert(0, os.path.abspath('../contrib/rcp')) # for argparse
sys.path.insert(0, os.path.abspath('../contrib/rcp/usage_example')) # for argparse
# -- Project information -----------------------------------------------------
@@ -42,6 +44,7 @@ extensions = [
"sphinx.ext.autosectionlabel",
"sphinx.ext.napoleon",
"pysim_fs_sphinx",
"sphinx.ext.graphviz",
]
# Add any paths that contain templates here, relative to this directory.
+1
View File
@@ -50,6 +50,7 @@ pySim consists of several parts:
suci-keytool
saip-tool
smpp-ota-tool
rcpf
Indices and tables
+690
View File
@@ -0,0 +1,690 @@
Remote Card Procedure Framework
===============================
The Remote Card Procedure Framework `(RCPF)` is a modular system to provide custom,
remote controlled, procedures to card `(UICC or eUICC)` holders. The card holder
uses a minimal client program `(RCP Client)` together with a PC/SC reader. The
client program will then connect to a remote server `(RCP Server)`. The remote
server maintains a list and connections to custom modules `(RCP Modules)`, where
each module implements a set of procedures (commands). Based on its internal list,
the remote server will offer a set of suitable commands to the client. The card
holder may then chose a command to request the execution of a specific remote
card procedure. The server will make the connection to the matching module and act
as a proxy between the module and the client program.
.. graphviz::
digraph foo {
subgraph cluster_server {
label = "server (card issuer)"
RCPS [label = "RCP Server"];
RCPM [label = "RCP Module"];
CKP [label = "CardKeyProvider"];
}
subgraph cluster_field {
label = "field (card holder)"
ICC [label = "UICC/eUICC"];
RCPC [label = "RCP Client"];
}
RCPC -> ICC [label="PC/SC, APDU"];
RCPC -> RCPS [label="WS, JSON"];
RCPS -> CKP [label="pgSQL or CSV"];
RCPS -> RCPM [label="WS, JSON", headlabel="n", taillabel="1", dir=both];
}
in case the procedure requires a secure channel, the key material is retrieved
using a `CardKeyProvider` [1]. Since the retrieval of the key material
as well as the secure channel establishment happens internally, the related
key material is never disclosed to the client side.
This solves a major problem many card deployments suffer from: Due to security
reasons it is not always be possible to disclose key material to the card
holder. This becomes a problem in case card contents have to be modified after
the card had been deployed. This often means that the card issuer has to
physically replace the already deployed cards. With `RCPF`, the card issuer can
replace this process by deploying a suitable RCP Module on his server to offer
a fix-up procedure that the card holder can call remotely.
[1] :ref:`Retrieving card-individual keys via CardKeyProvider`
In the following we will describe the system components in further detail. We
will also give an introduction on how users can implement custom `RCP Modules`
RCP Server
~~~~~~~~~~
The `RCP Server` is the core component in the overall system. It acts as a proxy
between the `RCP Modules` (see below) and the `RCP Client` (see below). The
`RCP Server` is permanently aware of which `RCP Modules` are available and knows
their properties. With this knowledge, the `RCP Server` is able to check which
module provides suitable procedures for specific card type.
Another responsibility of the `RCP Server` is to retrieve the key material using
the `CardKeyProvider`. As far as the `CardKeyProvider` is concerned, the RCP
Server takes the exact same commandline options as pySim-shell.py. However, in
case column encryption is used. The decryption key shall be passed to the
`RCP Module` instead to the `RCP Server`. This moves the decryption to the point
where the key material is actually needed.
To ensure the privacy of the traffic exchanged between `RCP Client`,
`RCP Server` and the `RCP Modules`, all links use SSL/TLS encrypted channels.
This is in particular relevant for the `RCP Client` which usually connects to
the `RCP Server` via the public internet.
Since the `RCP Server` is exposed to the public internet, it also requires some
level of protection against malicious requests. To minimize the risk arising
from malformed requests, each incoming and outgoing message is validated against
a JSON schema (also on the internal interfaces). Incoming requests from the
RCP Client side are also rate-limited to guard against excessive requests (DoS)
To monitor the `RCP Client` requests, the `RCP Server` support logging to an
`OpenObserve` monitoring entity. For each request exactly one report es
generated and sent to `OpenObserve`. For successful request, this report will
only contain metadata. In case of crashes or when the return code of the
`RCP Module` procedure is not 0, a full debug log is included as well.
.. argparse::
:module: contrib.rcp.rcp_server
:func: option_parser
:prog: contrib/rcp/rcp_server.py
RCP Client
~~~~~~~~~~
The `RCP Client` is used in the field by the card holder to request command
lists and to request the execution of procedures from the `RCP Server`.
The execution of a procedure is usually done in two steps. In the first step,
the card holder will request a list with available commands using the `--help`
option. The command list is then requested from the `RCP Server` displayed as
a regular commandline help-screen. The list will only contain commands, which
are actually suitable for the specific card type/model that card holder owns.
In the second step, the card holder will choose a command to request the
execution of the related procedure. In case the user already knows exactly
which command to execute, the first step may also be skipped. The request of
command lists for the purpose of displaying commandline help-screens is
entirely optional.
To avoid having to upgrade the RCP Client too often, the implementation is kept
as simple as possible. Technically, the RCP Client is not much more than a
proxy between a PC/SC-Reader and the RCP Server. All higher level tasks, like
requesting the ICCID (UICC or eSIM) or the EID (eUICC) are implemented on the
server side.
.. argparse::
:module: contrib.rcp.rcp_client
:func: option_parser
:prog: contrib/rcp/rcp_client.py
RCP Module
~~~~~~~~~~
The processing chain terminates in one of multiple `RCP Modules`. The `RCP Module`
is the custom implementation that implements one or more procedures. The
framework is designed in such a way that `RCP Modules` have minimal boilerplate
code. The implementation is kept simple. Users, which are familiar with
`pySim-shell` and its API will find the implementation of custom `RCP Modules`
as simple as implementing a new `pySim-shell` command.
From inside a procedure, the API user has access to the same objects (rs, card,
lchan) that are also usually available in `pySim-shell` environment.
To reset the card, retrieve the ATR and to exchange APDUs, the `pySim.transport`
API together with a custom `LinkBase (RcpsSimLink)` object is used. This means
that all modules which depend on the `pySim.transport` API can be used right
away without modification.
A procedure always runs in a dedicated thread, which means no special
precautions are necessary. A procedure may wait or sleep without disturbing
other requests.
Even though there are similarities to `pySim-shell` one has to keep in mind that
`RCP Modules` are intended to run non-interactively, which means they naturally
do not provide any support for `cmd2` API calls. This means that before code
from `pySim-shell` commands can be re-used, any `cmd2` entanglement must be
removed or separated otherwise.
.. argparse::
:module: contrib.rcp.usage_example.rcp_module
:func: option_parser
:prog: contrib/rcp/usage_example/rcp_module.py
Usage Example
~~~~~~~~~~~~~
All system components and related modules can be found in `contrib/rcp`. The
sub directory `usage_example` contains an example `RCP Module` and scripts to
make it easier to get started. The following steps explain in detail how to get
the `usage_example` running.
Parameters
----------
The `usage_example` contains a file `params.cfg`. This file contains variables,
which hold the parameters for the shell-scripts included in the example. The
parameters set up the system in such a way that everything runs locally.
Normally no changes are required, but it is strongly advised to review the
parameters to verify there are no clashes with other services.
Preparing Card Keys
-------------------
The example assumes a PC/SC reader and a `sysmoISIM-SJA5` or similar. To run
the `usage_example`, no modification to the card itself are required, but the
example key material (SCP02) in `card_data.csv` must match the test card.
The following example assumes that the card has the ICCID ``8949440000001155306``
and the following SCP02 keys:
+---------+----------------------------------+
| Keyname | Keyvalue |
+=========+==================================+
| ENC/KIC | F09C43EE1A0391665CC9F05AF4E0BD10 |
+---------+----------------------------------+
| MAC/KID | 01981F4A20999F62AF99988007BAF6CA |
+---------+----------------------------------+
| DEK/KIK | 8F8AEE5CDCC5D361368BC45673D99195 |
+---------+----------------------------------+
This would result into a `card_data.csv` file with the following content:
::
iccid,kic,kid,kik
8949440000001155306,F09C43EE1A0391665CC9F05AF4E0BD10,01981F4A20999F62AF99988007BAF6CA,8F8AEE5CDCC5D361368BC45673D99195
See also: :ref:`Retrieving card-individual keys via CardKeyProvider` and :ref:`Guide: Managing GP Keys`
When `card_data.csv` is re-aligned, the columns containing key material need to
be encrypted. This is done by running `encrypt_card_data.sh`. This script will
output a file `card_data.csv.encr` which contains the encrypted key material.
Running the RCP Server
----------------------
The `RCP Server` can be started using the included `start_rcp_server.sh` script.
::
$ ./start_rcp_server.sh
+ PYTHONPATH=../../../
+ ../../..//contrib/rcp/rcp_server.py --rcpc-server-addr 127.0.0.1 --rcpc-server-port 8000 --rcpc-server-cert ./certs/example_ssl_rcpc_rcps_cert.pem --rcpm-server-addr 127.0.0.1 --rcpm-server-port 8010 --rcpm-server-cert ./certs/example_ssl_rcpm_rcps_cert.pem --rcpm-module-ca-cert ./certs/example_ssl_rcp_ca_cert.crt --csv ./card_data.csv.encr
INFO: loading SSL/TLS CA certificate (RCP Module Command Server Client): ./certs/example_ssl_rcp_ca_cert.crt
INFO: Using CSV file as card key data source: ./card_data.csv.encr
WARNING: Reporting to OpenObserve: (disabled)
INFO: Rate-Limit: max 10 requests per sec.
INFO: RCP Client Server at: 127.0.0.1:8000
INFO: RCP Module server at: 127.0.0.1:8010
We can see that now to ports have been opened. `127.0.0.1:8000` is the port
where `RCP Clients` can connect. In a productive setup, this port would
normally be reachable from outside. The other port on `127.0.0.1:8010` is
accepting connections from `RCP Modules` This port should not be reachable
from the outside. It is intended to be used for the interprocess communication
between the `RCP Server` and the `RCP Modules`
In this state, the `RCP Server` waits for requests from both `RCP Clients` and
`RCP Modules`. However, there are not `RCP Modules` registered yet, so any
request from an `RCP Client` would be quilted with an error message.
Running the RCP Module
----------------------
For a functioning setup a suitable `RCP Module` is needed. The provided
`rcp_module.py` python program implements a few procedures which are suitable
for a `sysmoISIM-SJA5` card.
We can start the `RCP Module` with the provided start script
`start_rcp_module.sh`
::
$ ./start_rcp_module.sh
+ PYTHONPATH=../../../:../../..//contrib/rcp
+ ./rcp_module.py --uri wss://127.0.0.1:8010 --rcps-ca-cert ./certs/example_ssl_rcp_ca_cert.crt --rcpm-cmd-server-addr 127.0.0.1 --rcpm-cmd-server-port 8020 --rcpm-cmd-server-cert ./certs/example_ssl_rcps_rcpm_cert.pem --column-key kic:00112233445566778899AABBCCDDEEFF --column-key kid:00112233445566778899AABBCCDDEEFF --column-key kik:00112233445566778899AABBCCDDEEFF
INFO: RCP Module startup: rcp_module
INFO: loading SSL/TLS CA certificate (RCPM Server Client): ./certs/example_ssl_rcp_ca_cert.crt
INFO: RCPC command server at: 127.0.0.1:8020
The `RCP Module` is now connected to the `RCP Server`. The log output of the
`RCP Server` also confirms that there is a new `RCP Module` available.
::
INFO: new RCP module, RCP modules available: 'rcp_module'
On the output of the `RCP Module` we can see that the `RCP Module` has
opened another port on `127.0.0.1:8020`. This is where the `RCP Module` accepts
dedicated connections from the `RCP Server` when an `RCP Client` requests a
procedure. In an installation with multiple `RCP Modules`, each `RCP Module`
must use a dedicated port number.
Note that we also pass the column key for the key material using the
`--column-key` parameter. This parameter works exactly as in `pySim-shell`.
We supply the column key to the `RCP Module` and not to the `RCP Server`
move the decryption as close as possible to where it is needed.
Running the RCP Client
----------------------
The `usage_example` provides a shello-script `run_rcp_client.sh` that which
requests commandline help and requests procedures by calling other scripts.
However to get an understanding how the `RCP Client` is supposed to be used, it
makes more sense to call the sub scripts individually. We will now go through
step by step.
The first shell-script `./run_rcp_client_help.sh` assumes that the card holder
uses the `RCP Client` for the first time. He does not know which commandline
arguments are available, so he just calls `rcp_client.py` with the option `-h`.
::
$ ./run_rcp_client_help.sh
+ PYTHONPATH=../../../
+ ../../..//contrib/rcp/rcp_client.py -h
usage: rcp_client.py [-h] [-d DEV] [-b BAUD] [--pcsc-shared] [-p PCSC | --pcsc-regex REGEX] [--modem-device DEV] [--modem-baud BAUD] [--osmocon PATH]
[--apdu-trace] [--verbose] [--uri URI] [--ca-cert CA_CERT]
RCP Client
options:
-h, --help show this help message and exit
--apdu-trace Trace the command/response APDUs exchanged with the card (default: False)
--verbose Enable verbose logging (default: False)
--uri URI URI of the RCP-Server (default: None)
--ca-cert CA_CERT SSL/TLS CA-Certificate of the RCP-Server (default: None)
...
PC/SC Reader:
Use a PC/SC card reader to talk to the SIM card. PC/SC is a standard API for how applications access smart card readers, and is available on a variety of
operating systems, such as Microsoft Windows, MacOS X and Linux. Most vendors of smart card readers provide drivers that offer a PC/SC interface, if not even
a generic USB CCID driver is used. You can use a tool like ``pcsc_scan -r`` to obtain a list of readers available on your system.
--pcsc-shared Open PC/SC reaer in SHARED access (default: EXCLUSIVE) (default: False)
-p, --pcsc-device PCSC
Number of PC/SC reader to use for SIM access (default: None)
--pcsc-regex REGEX Regex matching PC/SC reader to use for SIM access (default: None)
...
From the output the card holder learns that there is an `--uri` parameter and
that the same PC/SC options like in `pySim-shell.py` are supported. There is
also a `--ca-cert` parameter where a CA certificate can be supplied in case the
`RCP Server` uses a self-signed CA (which applies to this example)
The second script `run_rcp_client_help_cmd.sh` assumes that the card holder now
knows that the minimum required parameters are the `--uri` of the `RCP Server`,
the `--ca-cert` of the `RCP Server` and `-p` to tell the `RCP Client` which PC/SC
reader to use.
::
$ ./run_rcp_client_help_cmd.sh
+ PYTHONPATH=../../../
+ ../../..//contrib/rcp/rcp_client.py --uri wss://127.0.0.1:8000 --ca-cert ./certs/example_ssl_rcp_ca_cert.crt -p 0 -h
INFO: loading SSL/TLS CA certificate (RCP Server CA): ./certs/example_ssl_rcp_ca_cert.crt
INFO: Using reader PCSC[Alcor Micro AU9540 00 00]
INFO: Detected Card with ATR: 3B9F96801F878031E073FE211B674A357530350265F8
INFO: RCP Server URI: wss://127.0.0.1:8000
INFO: Checking version ...
INFO: RCP Client version: software=1.0.0, protocol=1.0.0
INFO: RCP Server version: software=1.0.0, protocol=1.0.0
INFO: Requesting module descriptions from RCP Server ...
usage: rcp_client.py [-h] [-d DEV] [-b BAUD] [--pcsc-shared] [-p PCSC | --pcsc-regex REGEX] [--modem-device DEV] [--modem-baud BAUD] [--osmocon PATH]
[--apdu-trace] [--verbose] [--uri URI] [--ca-cert CA_CERT]
{rcp_module_reset,rcp_module_read_binary,rcp_module_read_record,rcp_module_unlock_aram} ...
RCP Client
positional arguments:
{rcp_module_reset,rcp_module_read_binary,rcp_module_read_record,rcp_module_unlock_aram}
RCP command to use
rcp_module_reset reset the card
rcp_module_read_binary
read binary data from a transparent file.
rcp_module_read_record
read binary data from a transparent file.
rcp_module_unlock_aram
unlock a locked ARA-M applet on a sysmoISIM-SJA5
...
The help screen now shows additional positional arguments. Those positional
arguments are the commands which the card holder can use to request a
procedure. In this example we have four procedures we can call:
`rcp_module_reset`, `rcp_module_read_binary`, `rcp_module_read_record`,
and `rcp_module_unlock_aram`
In the log output above the help screen, we can also see that a connection was
made and that the `RCP Client` has requested module descriptions from the
server. The `RCP Client` has sent the ATR of the card to the `RCP Server`. The
`RCP Server` has used this information to look through its internal list to
find modules which offer procedures suitable for this specific card.
The card holder now knows which commands or procedures are available, but he
still does not know if arguments are required and what those arguments are.
The third script `run_rcp_client_help_cmd_specific.sh` shows how the card
holder can request a dedicated help-screen for each of the commands.
::
$ ./run_rcp_client_help_cmd_specific.sh
...
+ PYTHONPATH=../../../
+ ../../..//contrib/rcp/rcp_client.py --uri wss://127.0.0.1:8000 --ca-cert ./certs/example_ssl_rcp_ca_cert.crt -p 0 rcp_module_read_record --help
INFO: loading SSL/TLS CA certificate (RCP Server CA): ./certs/example_ssl_rcp_ca_cert.crt
INFO: Using reader PCSC[Alcor Micro AU9540 00 00]
INFO: Detected Card with ATR: 3B9F96801F878031E073FE211B674A357530350265F8
INFO: RCP Server URI: wss://127.0.0.1:8000
INFO: Checking version ...
INFO: RCP Client version: software=1.0.0, protocol=1.0.0
INFO: RCP Server version: software=1.0.0, protocol=1.0.0
INFO: Requesting module descriptions from RCP Server ...
usage: rcp_client.py rcp_module_read_record [-h] --fid FID --record RECORD
options:
-h, --help show this help message and exit
--fid FID File identifier to of the file to read
--record RECORD File record to read
...
We can see in the log that the `RCP Client` again sends a request to the
`RCP Server` and retrieves the `RCP Module` descriptions. Then a dedicated
help-screen for the `rcp_module_read_record` command is displayed. Now the card
holder knows which parameters are required to perform the related procedure.
Until this point there was only interaction with the `RCP Client` and the
`RCP Server`. The `RCP Module` has not seen any requests yet. The provided
script `run_rcp_client_cmd.sh` illustrates how the card holder can run an
command that performs an actual procedure with the `RCP Module`.
::
$ ./run_rcp_client_cmd.sh
...
+ PYTHONPATH=../../../
+ ../../..//contrib/rcp/rcp_client.py --uri wss://127.0.0.1:8000 --ca-cert ./certs/example_ssl_rcp_ca_cert.crt -p 0 rcp_module_read_record --fid 3f00 --fid 2f00 --record 1
INFO: loading SSL/TLS CA certificate (RCP Server CA): ./certs/example_ssl_rcp_ca_cert.crt
INFO: Using reader PCSC[Alcor Micro AU9540 00 00]
INFO: Detected Card with ATR: 3B9F96801F878031E073FE211B674A357530350265F8
INFO: RCP Server URI: wss://127.0.0.1:8000
INFO: Checking version ...
INFO: RCP Client version: software=1.0.0, protocol=1.0.0
INFO: RCP Server version: software=1.0.0, protocol=1.0.0
INFO: Requesting module descriptions from RCP Server ...
INFO: Executing command with RCP Server ...
INFO: RcpcCltConnHdlr(140335960510480) -- reading linear-fixed file: ['3f00', '2f00'] ...
INFO: RcpcCltConnHdlr(140335960510480) -- file content is: 61294F10A0000000871002FFFFFFFF890709000050055553696D31730EA00C80011781025F608203454150
INFO: Command execution done, rc: 0
The example reads record 1 from the file ``3F00/2F00`` and returns the file
content. We also can see by the return code that the procedure was successful.
The return code is also passed to `sys.exit()`, so that the card holder can
use it in a script.
The APDUs required to perform this action were entirely generated under the
control of the `RCP Module`. In the log of the `RCP Server` we can see which
command was executed on which `RCP Module` was used. We also see the return
code here as well.
::
...
INFO: RcpcSrvConnHdlr(140093766623552) -- executing procedure for command "rcp_module_read_record" on module "rcp_module" at: wss://127.0.0.1:8020
INFO: RcpcSrvConnHdlr(140093766623552) -- command execution done, rc: 0
...
In the log of the `RCP Module` we can follow up on how the procedure was
carried out.
::
...
INFO: RcpmCmdSrvConnHdlr(140156091028880) -- executing command: rcp_module_read_record ['--fid', '3f00', '--fid', '2f00', '--record', '1']
INFO: Waiting for card...
INFO: Card is of type: UICC
INFO: Detected UICC Add-on "SIM"
INFO: Detected UICC Add-on "GSM-R"
INFO: Detected UICC Add-on "RUIM"
WARNING: EF.DIR seems to be empty!
INFO: ADF.ISD: a000000003000000
INFO: ARA-M: a00000015141434c00
INFO: ISIM: a0000000871004
INFO: USIM: a0000000871002
INFO: Detected CardModel: SysmocomSJA5
INFO: RcpmCmdSrvConnHdlr(140156091028880) -- reading linear-fixed file: ['3f00', '2f00'] ...
INFO: RcpmCmdSrvConnHdlr(140156091028880) -- file content is: 61294F10A0000000871002FFFFFFFF890709000050055553696D31730EA00C80011781025F608203454150
INFO: RcpmCmdSrvConnHdlr(140156091028880) -- command execution done, rc: 0
...
In first line we see the command and its parameters. The lines that follow will
look familiar to `pySim-shell` users. The last three log lines carry the print
statements which we also see in the log messages on the `RCP Client`. The last
line informs about the conclusion of the procedure and also shows the return
code.
Implementing an RCP Module
~~~~~~~~~~~~~~~~~~~~~~~~~~
To make use of the Remote Card Procedure Framework, it is eventually necessary
to implement a custom `RCP Module`. In the following section, we will go
through the implementation of the `RCP Module` that is provided with the
`usage_example`.
NOTE: much of the following is explained in greater detail in the comments
found in `rcp_module_utils.py`.
Overview
~~~~~~~~
`RCP Modules` are normal python programs that can started directly from the
command prompt. However, due to the location of the file it is necessary that
`PYTHONPATH` points to the location of the `pySim` modules as well as to the
modules found in `contrib/rcp` (see `start_rcp_module.sh` for reference).
As mentioned earlier `RCP Modules` may use the `pySim` API like any other
`pySim` program, given that there is no dependency to `cmd2`. So it is no
surprise that we find some `pySim` modules in the import section of the
provided example.
The utilities required to implement an `RCP Module` are imported from
`rcp_module_utils.py`. From this module we import two functions
`rcpm_setup_argparse` and `rcpm_run_module` and the two classes `RCP Module`
and `RcpModuleHdlr`.
Function: rcpm_setup_argparse
------------------------------
The first function `rcpm_setup_argparse` returns an argument parser that is already
equipped with the basic commandline arguments that an `RCP Module` needs. In
case the specific `RCP Module` implementation requires additional arguments,
those can be added using normal `argparse` API calls.
Function: rcpm_run_module
-------------------------
The second function `rcpm_run_module` is used to run the `RCP Module`. This
function gets the parsed commandline options (`opts`) and the `RcpModule` class
(`module`) as parameters. In addition to that, `rcpm_run_module` also accepts
custom `*args` and `**kwargs` arguments, which are passed to the constructor of
the `RcpModule` class.
When `rcpm_run_module` is called. It registers the `RCP Module` and starts the
RCP Client command server. It also takes care of the proper instantiation of the
`RcpModule` class, which were passed with the `module` parameter.
Class: RcpModule
----------------
The Class `RcpModule` is the base class that is used to create a concrete
`RCP Module` implementaion. Through this class, the API user defines the
properties of the `RCP Module` as well as the command methods, which implement
the related `Remote Card Procedures`.
Class: RcpModuleHdlr
--------------------
The class `RcpModuleHdlr` is used by the framework to instantiate a handler
object (`hdlr`), which is passed to each of the aforementioned command methods.
The handler object is used as a vehicle to provide access the resources we need
to send APDUs, print messages on the `RCP Client`, etc.
Module Properties
~~~~~~~~~~~~~~~~~
Before we can define any module properties, we first need to create a derived
class from the `RcpModule` class we have imported from `rcp_module_utils.py`.
In that class, we then define the basic properties of the `RCP Module`.
name
----
Each `RCP Module` needs a distinct name. The name must not collide with the
names of other `RCP Modules`. The name uniquely identifies the `RCP Module` and
is used as a prefix for the command names used with the user interface of the
`RCP Client`. Therefore a short name is desirable.
cmd_descr
---------
The `cmd_descr` property defines the command properties. Since an `RCP Module`
may offer multiple commands (procedures), this property is an array, where
each item holds the definition for one specific command.
The command definitions are formatted as a python dict. Like the `RCP Module`
itself, each command has a `name`. As mentioned before. This name is concatenated
with the name of the `RCP Module`.
Each command definition also gets a `help` string. The help string will show up
in the commandline help of the `RCP Client`. It should be short and concise.
Command definitions also need to define commandline arguments. For this an
`args` array is added to the command definition as well. In case no arguments
are provided. The array is empty. Otherwise it will contain one or more dict
members, where each specifies a `name` and a `spec`. The `name` sets the
argument name (e.g. --fid), and the `spec` specifies the properties of the
argument. The concept is borrowed from `argparse` and works very similar. API
users can specify `required`, `help`, `default` and a type. However, to avoid
name-space collisions, the type field is called `pytype` and the type identifier
must be passed as a string (e.g. 'int').
In case a procedure requires key material from the `CardKeyProvider`, the API
user may add a `get_keys` field to the command definition. In case eUICC keys
are needed. The API user will add a dict member with key `euicc` and populate
the value with an array that holds the column names of the columns where the
keys are found. The same also works for UICC keys by using 'uicc' as dict key.
When `get_keys` is correctly populated and the correct column keys are supplied
to the `RCP Module` at runtime. The `RCP Framework` will automatically retrieve
the key material, decrypt it and make it available to the related command
method.
suitable_for
------------
`suitable_for` is the third and last property, the API user must define. This
property holds an array where each member is a dict that defines a distinct
property of the card for which the module is suitable for. The `RCP Server`
uses this information to see which modules are suitable for a specific request.
As of now, the only property we can use to make the distinction, is the ATR of
the card.
custom resources
----------------
In case an RCP Module requires custom resources, those may be initialized using
a custom constructor in the derived class. This constructor receives the
`*args` and `**kwargs` arguments passed to `rcpm_run_module`. However, this is
an optional step. In case no constructor is defined, the default constructor
is used.
In addition to that, the API user may also define additional properties and
methods, provided they do not collide with existing methods of the base class
`RcpModule`.
Command Methods
~~~~~~~~~~~~~~~
Command methods are essentially normal python methods. However, since those
methods are called by the `RCP Framework`, they must follow a distinct scheme,
which we will go through in the following.
Each command defined in `cmd_descr` requires a corresponding command method. A
command method is always prefixed with `cmd_`. Then the exact name of the
command follows as defined in `cmd_descr`. For example if we have defined a
command with the name `read_record`, we must also define a method with the name
`cmd_read_record`.
The parameter list of a command method always contains only `self` and `hdlr`.
The `hdlr` parameter is the handler object (`RcpModuleHdlr`) through which we
access the resources provided by the `RCP Framework`.
Inside a command method, the API user is free to perform any task he wants.
Command Methods always run in a dedicated thread and may sleep or wait at any
time without disturbing running procedures from other requestors.
A command method should always return an integer as return code. In case the
procedure ends successfully, the return code shall be `0`. The return code is
passed through to the `RCP Client`, which returns it on exit to the operating
system
Handler Resources
~~~~~~~~~~~~~~~~~
As mentioned earlier, a commend method receives a handler object via
the `hdlr` parameter. This object is of type `RcpModuleHdlr` and vaguely
comparable to the `app` (`PysimApp`) object found in `pySim-shell.py`.
The handler object provides the command method with the resources it needs to
perform the card procedure.
rs, card, lchan
---------------
The Runtime State (`rs`), the Card (`card`) and the Lchan (`lchan`) Object
have the same objectives asn in `pySim-shell.py`. Those objects work and are
used the same way as they would in `pySim-shell.py`. It is assumed that the
API user is already familiar with those objects.
cmd_args
--------
The command arguments (`cmd_args`) contains the command line arguments as they
were passed by the card holder on the `RCP Client` commandline in the form of
a `Namespace` object.
Even though the command arguments are syntax-checked against the `args`
description given in `cmd_descr`, caution is required to avoid security
problems arising from malicious input.
keys_uicc and keys_euicc
------------------------
In case key material was requested via the `get_keys` in `cmd_descr`,
`keys_uicc` and `keys_euicc` will contain those keys in the form of a dict. The
dict key is the is the `CardKeyProvider` column name and the related dict value
is the key material in its decrypted form.
When accessing `keys_uicc` and `keys_euicc`, extra care should be taken. It may
make sense to delete/overwrite those dictionaries as soon as the keys were used
for the intended purpose. However, due to python's internal memory management
key material may remain longer in the system memory as expected.
print
-----
The `hdlr` object also provides a `print` method. This method accepts a string
as the only parameter and can be used to display custom messages in the log
output of the `RCP Client`. The method can be used to inform the card holder
about the progress of a procedure or to print error messages in case a
procedure fails.