13 Commits

Author SHA1 Message Date
Neels Hofmeyr
d8f3c78135 SmspTpScAddr: set example_input
Change-Id: Ie2c367788215d746807be24051478f0032a19448
2026-03-04 00:24:02 +01:00
Neels Hofmeyr
6b9b46a5a4 MilenageRotationConstants: set example_input to 3GPP default
Change-Id: I36a9434b2f96d26d710f489d5afce1f0ef05bba1
2026-03-04 00:23:41 +01:00
Philipp Maier
b6b4501e37 contrib/smpp-ota-tool: fix boolean commandline parameters
Boolean parameters should be false by default and use store_true when
set.

Change-Id: I0652b48d2ea5efbaaf5bc147aa8cef7ab8b0861d
Related: OS#6868
2026-02-24 09:52:48 +01:00
Philipp Maier
54658fa3a9 contrib/smpp-ota-tool: add missing usage helpstrings
Change-Id: Ic1521ba11b405f311a30fdb3585ad518375669ae
Related: OS#6868
2026-02-24 09:52:48 +01:00
Philipp Maier
eb04bb1082 contrib/smpp-ota-tool: warn about mixed up KIC/KIC indexes
Cards usually have multiple sets of KIC, KID (and KIK). The keys
are selected through an index. However, mixing keys from different
sets is concidered as a security violation and cards should reject
such configurations.

Let's print a warning to make users aware that something is off.

Change-Id: Ieb4e14145baba1c2cb4a237b612b04694940f402
Related: OS#6868
2026-02-24 09:52:48 +01:00
Philipp Maier
453fde5a3a contrib/smpp-ota-tool: use correct kid index
(normally KID index and KIC index should be the same since mixing keys
is a concidered as a security violation. However, in this tool we
want to allow users to specify different indexes for KIC and KIC so that
they can make tests to make sure their cards correctly reject mixed up
key indexes)

Change-Id: I8847ccc39e4779971187e7877b8902fca7f8bfc1
Related: OS#6868
2026-02-17 15:24:25 +01:00
Philipp Maier
57237b650e contrib/smpp-ota-tool/cosmetic: use lazy formatting for logging
Change-Id: I2540472a50b7a49b5a67d088cbdd4a2228eef8f4
Related: OS#6868
2026-02-17 15:24:25 +01:00
Philipp Maier
1f94791240 contrib/smpp-ota-tool/cosmetic: fix sourcecode formatting
Change-Id: Icbce41ffac097d2ef8714bc8963536ba54a77db2
Related: OS#6868
2026-02-17 15:24:25 +01:00
Philipp Maier
1a28575327 pySim-shell_test/euicc: ensure test-profile is enabled
When testing commands like get_profile_info, enable_profile,
disable_profile or the commands to manage notifications, we
should ensure that the correct profile is enabled before
executing the actual testcase.

Change-Id: Ie57b0305876bc5001ab3a9c3a3b5711408161b74
2026-02-17 09:22:42 +00:00
Neels Hofmeyr
e7016b5b57 compile_asn1_subdir: filter compiled files by .asn suffix
When I open the .asn file in vim, pySim should not attempt to read the
vim .swp file as asn.1.

	  File "/home/moi/osmo-dev/src/pysim/pySim/esim/saip/__init__.py", line 45, in <module>
	    asn1 = compile_asn1_subdir('saip')
	[...]
	  File "<frozen codecs>", line 325, in decode
	UnicodeDecodeError: 'utf-8' codec can't decode byte 0xad in position 21: invalid start byte

Related: OS#6937
Change-Id: I37df3fc081e51e2ed2198876c63f6e68ecc8fcd8
2026-02-10 16:14:14 +00:00
Philipp Maier
e80f3160a9 pySim/euicc: fix encoding/decoding of Iccid
The class Iccid uses a BcdAdapter to encoded/decode the ICCID. This
works fine for ICCIDs that have an even (20) number of digits. In case
the digit count is odd (19), the ICCID the last digit requires padding.

Let's switch to PaddedBcdAdapter for encoding/decoding, to ensure that
odd-length ICCIDs are padded automatically.

Change-Id: I527a44ba454656a0d682ceb590eec6d9d0ac883a
Related: OS#6868
2026-02-10 13:26:45 +00:00
Neels Hofmeyr
917ad7f9f5 gitignore: fix vim swp file pattern
Change-Id: I5a8351dc09f6ca7c8e9032ff8352e5cf1a4833a3
2026-02-10 13:10:17 +00:00
Philipp Maier
8b2a49aa8e esim/http_json_api: add alternative API interface (follow up)
This is a follow up patch to change:
I2a5d4b59b12e08d5eae7a1215814d3a69c8921f6

- do not ignore length of kwargs
- fix role parameter (roles other than 'legacy_client' can be used now)
- use startswith instead of match

Related: SYS#7866
Change-Id: Ifae13e82d671ff09bddf771f063a388d2ab283eb
2026-02-10 13:42:44 +01:00
11 changed files with 41 additions and 23 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
*.pyc *.pyc
.*.swp .*.sw?
/docs/_* /docs/_*
/docs/generated /docs/generated

View File

@@ -141,7 +141,7 @@ class SmppHandler:
tuple containing the last response data and the last status word as byte strings tuple containing the last response data and the last status word as byte strings
""" """
logger.info("C-APDU sending: %s..." % b2h(apdu)) logger.info("C-APDU sending: %s...", b2h(apdu))
# translate to Secured OTA RFM # translate to Secured OTA RFM
secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu) secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu)
@@ -194,19 +194,19 @@ if __name__ == '__main__':
option_parser.add_argument('--tar', required=True, type=is_hexstr, help='Toolkit Application Reference') option_parser.add_argument('--tar', required=True, type=is_hexstr, help='Toolkit Application Reference')
option_parser.add_argument("--cntr_req", choices=CNTR_REQ.decmapping.values(), default='no_counter', option_parser.add_argument("--cntr_req", choices=CNTR_REQ.decmapping.values(), default='no_counter',
help="Counter requirement") help="Counter requirement")
option_parser.add_argument('--ciphering', default=True, type=bool, help='Enable ciphering') option_parser.add_argument('--no-ciphering', action='store_true', default=False, help='Disable ciphering')
option_parser.add_argument("--rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc', option_parser.add_argument("--rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc',
help="message check (rc=redundency check, cc=crypt. checksum, ds=digital signature)") help="message check (rc=redundency check, cc=crypt. checksum, ds=digital signature)")
option_parser.add_argument('--por-in-submit', default=False, type=bool, option_parser.add_argument('--por-in-submit', action='store_true', default=False,
help='require PoR to be sent via SMS-SUBMIT') help='require PoR to be sent via SMS-SUBMIT')
option_parser.add_argument('--por-shall-be-ciphered', default=True, type=bool, help='require encrypted PoR') option_parser.add_argument('--por-no-ciphering', action='store_true', default=False, help='Disable ciphering (PoR)')
option_parser.add_argument("--por-rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc', option_parser.add_argument("--por-rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc',
help="PoR check (rc=redundency check, cc=crypt. checksum, ds=digital signature)") help="PoR check (rc=redundency check, cc=crypt. checksum, ds=digital signature)")
option_parser.add_argument("--por_req", choices=POR_REQ.decmapping.values(), default='por_required', option_parser.add_argument("--por_req", choices=POR_REQ.decmapping.values(), default='por_required',
help="Proof of Receipt requirements") help="Proof of Receipt requirements")
option_parser.add_argument('--src-addr', default='12', type=str, help='TODO') option_parser.add_argument('--src-addr', default='12', type=str, help='SMS source address (MSISDN)')
option_parser.add_argument('--dest-addr', default='23', type=str, help='TODO') option_parser.add_argument('--dest-addr', default='23', type=str, help='SMS destination address (MSISDN)')
option_parser.add_argument('--timeout', default=10, type=int, help='TODO') option_parser.add_argument('--timeout', default=10, type=int, help='Maximum response waiting time')
option_parser.add_argument('-a', '--apdu', action='append', required=True, type=is_hexstr, help='C-APDU to send') option_parser.add_argument('-a', '--apdu', action='append', required=True, type=is_hexstr, help='C-APDU to send')
opts = option_parser.parse_args() opts = option_parser.parse_args()
@@ -214,18 +214,22 @@ if __name__ == '__main__':
format='%(asctime)s %(levelname)s %(message)s', format='%(asctime)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S') datefmt='%Y-%m-%d %H:%M:%S')
if opts.kic_idx != opts.kid_idx:
logger.warning("KIC index (%s) and KID index (%s) are different (security violation, card should reject message)",
opts.kic_idx, opts.kid_idx)
ota_keyset = OtaKeyset(algo_crypt=opts.algo_crypt, ota_keyset = OtaKeyset(algo_crypt=opts.algo_crypt,
kic_idx=opts.kic_idx, kic_idx=opts.kic_idx,
kic=h2b(opts.kic), kic=h2b(opts.kic),
algo_auth=opts.algo_auth, algo_auth=opts.algo_auth,
kid_idx=opts.kic_idx, kid_idx=opts.kid_idx,
kid=h2b(opts.kid), kid=h2b(opts.kid),
cntr=opts.cntr) cntr=opts.cntr)
spi = {'counter' : opts.cntr_req, spi = {'counter' : opts.cntr_req,
'ciphering' : opts.ciphering, 'ciphering' : not opts.no_ciphering,
'rc_cc_ds': opts.rc_cc_ds, 'rc_cc_ds': opts.rc_cc_ds,
'por_in_submit':opts.por_in_submit, 'por_in_submit': opts.por_in_submit,
'por_shall_be_ciphered':opts.por_shall_be_ciphered, 'por_shall_be_ciphered': not opts.por_no_ciphering,
'por_rc_cc_ds': opts.por_rc_cc_ds, 'por_rc_cc_ds': opts.por_rc_cc_ds,
'por': opts.por_req} 'por': opts.por_req}
apdu = h2b("".join(opts.apdu)) apdu = h2b("".join(opts.apdu))

View File

@@ -54,6 +54,8 @@ def compile_asn1_subdir(subdir_name:str, codec='der'):
__ver = sys.version_info __ver = sys.version_info
if (__ver.major, __ver.minor) >= (3, 9): if (__ver.major, __ver.minor) >= (3, 9):
for i in resources.files('pySim.esim').joinpath('asn1').joinpath(subdir_name).iterdir(): for i in resources.files('pySim.esim').joinpath('asn1').joinpath(subdir_name).iterdir():
if not i.name.endswith('.asn'):
continue
asn_txt += i.read_text() asn_txt += i.read_text()
asn_txt += "\n" asn_txt += "\n"
#else: #else:

View File

@@ -19,7 +19,6 @@ import abc
import requests import requests
import logging import logging
import json import json
from re import match
from typing import Optional from typing import Optional
import base64 import base64
from twisted.web.server import Request from twisted.web.server import Request
@@ -211,7 +210,7 @@ class JsonHttpApiFunction(abc.ABC):
# additional custom HTTP headers (server responses) # additional custom HTTP headers (server responses)
extra_http_res_headers = {} extra_http_res_headers = {}
def __new__(cls, *args, role = None, **kwargs): def __new__(cls, *args, role = 'legacy_client', **kwargs):
""" """
Args: Args:
args: (see JsonHttpApiClient and JsonHttpApiServer) args: (see JsonHttpApiClient and JsonHttpApiServer)
@@ -221,14 +220,13 @@ class JsonHttpApiFunction(abc.ABC):
# Create a dictionary with the class attributes of this class (the properties listed above and the encode_ # Create a dictionary with the class attributes of this class (the properties listed above and the encode_
# decode_ methods below). The dictionary will not include any dunder/magic methods # decode_ methods below). The dictionary will not include any dunder/magic methods
cls_attr = { attr_name: getattr(cls, attr_name) for attr_name in dir(cls) if not match("__.*__", attr_name) } cls_attr = {attr_name: getattr(cls, attr_name) for attr_name in dir(cls) if not attr_name.startswith('__')}
# Normal instantiation as JsonHttpApiFunction: # Normal instantiation as JsonHttpApiFunction:
if len(args) == 0: if len(args) == 0 and len(kwargs) == 0:
return type(cls.__name__, (abc.ABC,), cls_attr)() return type(cls.__name__, (abc.ABC,), cls_attr)()
# Instantiation as as JsonHttpApiFunction with a JsonHttpApiClient or JsonHttpApiServer base # Instantiation as as JsonHttpApiFunction with a JsonHttpApiClient or JsonHttpApiServer base
role = kwargs.get('role', 'legacy_client')
if role == 'legacy_client': if role == 'legacy_client':
# Deprecated: With the advent of the server role (JsonHttpApiServer) the API had to be changed. To maintain # Deprecated: With the advent of the server role (JsonHttpApiServer) the API had to be changed. To maintain
# compatibility with existing code (out-of-tree) the original behaviour and API interface and behaviour had # compatibility with existing code (out-of-tree) the original behaviour and API interface and behaviour had

View File

@@ -352,6 +352,7 @@ class SmspTpScAddr(ConfigurableParameter):
strip_chars = ' \t\r\n' strip_chars = ' \t\r\n'
max_len = 21 # '+' and 20 digits max_len = 21 # '+' and 20 digits
min_len = 1 min_len = 1
example_input = '+49301234567'
@classmethod @classmethod
def validate_val(cls, val): def validate_val(cls, val):
@@ -627,7 +628,7 @@ class MilenageRotationConstants(BinaryParam, AlgoConfig):
name = 'MilenageRotation' name = 'MilenageRotation'
algo_config_key = 'rotationConstants' algo_config_key = 'rotationConstants'
allow_len = 5 # length in bytes (from BinaryParam) allow_len = 5 # length in bytes (from BinaryParam)
example_input = '0a 0b 0c 0d 0e' example_input = '40 00 20 40 60'
@classmethod @classmethod
def validate_val(cls, val): def validate_val(cls, val):

View File

@@ -181,7 +181,7 @@ class SeqNumber(BER_TLV_IE, tag=0x80):
class NotificationAddress(BER_TLV_IE, tag=0x0c): class NotificationAddress(BER_TLV_IE, tag=0x0c):
_construct = Utf8Adapter(GreedyBytes) _construct = Utf8Adapter(GreedyBytes)
class Iccid(BER_TLV_IE, tag=0x5a): class Iccid(BER_TLV_IE, tag=0x5a):
_construct = BcdAdapter(GreedyBytes) _construct = PaddedBcdAdapter(GreedyBytes)
class NotificationMetadata(BER_TLV_IE, tag=0xbf2f, nested=[SeqNumber, ProfileMgmtOperation, class NotificationMetadata(BER_TLV_IE, tag=0xbf2f, nested=[SeqNumber, ProfileMgmtOperation,
NotificationAddress, Iccid]): NotificationAddress, Iccid]):
pass pass

View File

@@ -15,7 +15,7 @@
}, },
{ {
"profile_info": { "profile_info": {
"iccid": "8949449999999990031f", "iccid": "8949449999999990031",
"isdp_aid": "a0000005591010ffffffff8900001200", "isdp_aid": "a0000005591010ffffffff8900001200",
"profile_state": "disabled", "profile_state": "disabled",
"service_provider_name": "OsmocomSPN", "service_provider_name": "OsmocomSPN",

View File

@@ -23,7 +23,7 @@ import os
import json import json
from utils import * from utils import *
# This testcase requires a sysmoEUICC1-C2T with the test prfile TS48V1-B-UNIQUE (ICCID 8949449999999990031f) # This testcase requires a sysmoEUICC1-C2T with the test prfile TS48V1-B-UNIQUE (ICCID 8949449999999990031)
# installed, and in disabled state. Also the profile must be installed in such a way that notifications are # installed, and in disabled state. Also the profile must be installed in such a way that notifications are
# generated when the profile is disabled or enabled (ProfileMetadata) # generated when the profile is disabled or enabled (ProfileMetadata)

View File

@@ -3,6 +3,9 @@ set echo true
select ADF.ISD-R select ADF.ISD-R
# Ensure that the test-profile we intend to test with is actually enabled
enable_profile --iccid 89000123456789012341
# by ICCID (pre-installed test profile on sysmoEUICC1-C2T) # by ICCID (pre-installed test profile on sysmoEUICC1-C2T)
disable_profile --iccid 89000123456789012341 > enable_disable_profile.tmp disable_profile --iccid 89000123456789012341 > enable_disable_profile.tmp
enable_profile --iccid 89000123456789012341 >> enable_disable_profile.tmp enable_profile --iccid 89000123456789012341 >> enable_disable_profile.tmp

View File

@@ -3,6 +3,11 @@ set echo true
select ADF.ISD-R select ADF.ISD-R
# Generate two (additional) notifications by quickly enabeling the test profile # Ensure that the test-profile is actually enabled. (In case te test-profile
enable_profile --iccid 8949449999999990031f # was disabled, a notification may be generated. The testcase should tolerate
# that)
enable_profile --iccid 89000123456789012341
# Generate two (additional) notifications by quickly enabeling the test profile
enable_profile --iccid 8949449999999990031
enable_profile --iccid 89000123456789012341 enable_profile --iccid 89000123456789012341

View File

@@ -1,5 +1,10 @@
set debug true set debug true
set echo true set echo true
# The output of get_profiles_info will also include the "profile_state", which
# can be either "enabled" or "disabled". Ensure that the correct profile is
# enabled.
enable_profile --iccid 89000123456789012341
select ADF.ISD-R select ADF.ISD-R
get_profiles_info > get_profiles_info.tmp get_profiles_info > get_profiles_info.tmp