Compare commits

...

18 Commits

Author SHA1 Message Date
Neels Hofmeyr 23ac6d9a98 personalization: implement Gid1, Gid2 parameters
Change-Id: Ibd24962fdfe53a260df09d01b4ca1cc6fe2c4c35
2026-10-10 00:10:57 +02:00
Neels Hofmeyr 61537273db personalization.py parameter description tweak
Change-Id: I07a28ecd07461b585068ed16e8e6fa4881747b98
2026-10-10 00:10:57 +02:00
Neels Hofmeyr cb0509fdb4 saip.batch: log parameter errors
Change-Id: I6a46b2dc9018078ab8361226d1e6b50d3b4e1aaa
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr f4e90641e4 debug log for rebuild_mandatory_services()
Change-Id: Ie53c84247a9524a81b475b3b02b5330207c15601
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr a6fbc812a3 ConfigurableParameter.get_typical_input_len: limit to 10 lines
Calling applications can use get_typical_input_len() to decide for the
size of an input field. For very long input, a text input field
typically scrolls. So set an upper limit for get_typical_input_len().

Change-Id: Ia3d79e786f397a02bf2a8fafac5030d1198d9f76
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr e36256fad7 TuakNumberOfKeccak: remove numeric_base to hide random number sources
The purpose of numeric_base is to indicate for "random" values whether
to use decimal or hexadecimal digits. Indicate that TuakNumberOfKeccak
does not make sense to be randomized by not indicating a numeric_base.

Change-Id: I913878e3f05ad1e39ff45da75c67582a6a1f930f
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr e685692818 saip/personalization: add SUCI parameters for SAIP 2.1 (GFM)
Add GfmSuciCalcInfo, GfmSuciRi, and test.

Change-Id: I1b69debf5992aa715171b43b30864dc152dc556f
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr c4e92c7828 SAIP2.1_gfmsuci.der
Change-Id: I69354754013e4a98d86a9a8abdd7a6a36eb95177
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr 6882cca63c saip/personalization: add EuiccMandatoryServiceParam for 3 services
Even though the eUICC-Mandatory-services are always set by
saip.PES.rebuild_mandatory_services(), these new params are useful to
audit and validate the values as present in a given DER.

Change-Id: Icddeb2488c4a024c6ee5afcc1b6c8cc0e436c43c
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr 69f4c1d8f5 personalization: SUCI: ff-pad remaining file size
Change-Id: I9465eac9269e3f76dddd467109f547489a0feb93
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr 3cbeeb7569 saip/personalization: add SUCI parameters for SAIP 2.3
Change-Id: I7c727b10a850651a631211e4f168df5fec1600e5
Jenkins: skip-card-test
2026-10-10 00:10:57 +02:00
Neels Hofmeyr 43a9f65779 personalization: EnumParam: implement as value_map, not enum.IntEnum
Because of (1) and (2) below, using enum.IntEnum is not a good choice
for EnumParam. Use a plain dict as value map.

EnumParam's idea is to be a mapping
    human-readable-value-for-UI -> PES-value
and with enum.IntEnum we...

(1) ...we impose python namespace rules onto
human-readable-value-for-UI: "My Value" is not allowed, only "my_value".
By using enum.IntEnum, we make it impossible to use human readable
values like:
- "SUCI-on" (dash not allowed in python identifier)
- "True" (python keyword not allowed)
- "2" (numeric constant cannot be a python identifier)

(2) ...we limit PES-values to int. The mapping should support any type
that the given PES attribute needs (int, bool, bytes, ...).

Future: upcoming ConfigurableParameters depend on this patch:
- MncLen (labels "2" and "3")
- EuiccMandatoryServiceParam (values True and False)
- EfUstServiceParam like SuciInUsim (labels "SUCI-in-UE" and
  "SUCI-in-USIM")

History: this is near the first version of EnumParam, before it was
modified by fixeria to use enum.IntEnum, as requested by CR. I realize
now that enum.IntEnum will not work for upcoming eSIM parameters.

Change-Id: I690ceccf0ec7ef7067bcaa5cec1303cdaf0f78a4
Jenkins: skip-card-test
2026-10-09 23:51:18 +02:00
Neels Hofmeyr 9aa8934da2 saip/batch: add blacklist to to_csv_rows(), improve doc
Allow calling code to mute specific parameters from an output CSV.

For example, the SUCI CalcInfo data should be identical for all profiles
of a batch, and is also very long data. After this patch, an application
can name specific parameters to omit from an output CSV.

Change-Id: I5e567e59a007cf4b5d75a4dcea5371ff4404bf24
Jenkins: skip-card-test
2026-10-09 11:46:42 +00:00
Neels Hofmeyr 5173a81498 personalization: EF_SMSP: keep same length as found in template
Leave the decision for the record length to the template: callers can
can modify the value in the template without having to patch this code.

Change-Id: Id24752101ae82c4986209f4103cc9cbdcce8ce1d
Jenkins: skip-card-test
2026-10-09 11:46:42 +00:00
Neels Hofmeyr 24c24cae56 tweak test_configurable_parameters.py: show value found in template
Change-Id: If9039bbb8547ee24ae784a932f60cd5de6c9247b
Jenkins: skip-card-test
2026-10-09 11:46:42 +00:00
Neels Hofmeyr d9f2458566 test_configurable_parameters.py: tweak logging
This helped in figuring out some bug -- show all available context info
in raised exceptions.

Change-Id: I38e85ce0902f2ec4db63e158770b6d2cecb2dd30
Jenkins: skip-card-test
2026-10-09 11:46:42 +00:00
Neels Hofmeyr 4ba09440bf esim/http_json_api.py: support text/plain response Content-Type
Allow returning text/plain Content-Types as 'data' output argument.

So far, all the esim/http_json_api functions require a JSON response.
However, a specific vendor has a list function where the request is JSON
but the response is text/plain CSV data. Allow and return in a dict.

Change-Id: Iba6e4cef1048b376050a435a900c0f395655a790
Jenkins: skip-card-test
2026-10-09 11:46:42 +00:00
Eric Salem 23a6fb93f9 cat: Fix alpha identifier
Add implementation so that it prints.

Change-Id: I01aaeb84480c337e6328ef4e20321018525bd9d4
Signed-off-by: Eric Salem <ericsalem@gmail.com>
2026-10-05 14:49:35 -05:00
8 changed files with 3660 additions and 1533 deletions
+2 -3
View File
@@ -24,7 +24,7 @@ from construct import Int8ub, Int16ub, Byte, BitsInteger
from construct import Struct, Enum, BitStruct, this from construct import Struct, Enum, BitStruct, this
from construct import Switch, GreedyRange, FlagsEnum, Adapter from construct import Switch, GreedyRange, FlagsEnum, Adapter
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes from osmocom.construct import PlmnAdapter, BcdAdapter, GsmOrUcs2Adapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
from osmocom.utils import b2h, h2b from osmocom.utils import b2h, h2b
from pySim.utils import dec_xplmn_w_act from pySim.utils import dec_xplmn_w_act
@@ -37,8 +37,7 @@ class Address(COMPR_TLV_IE, tag=0x86):
# TS 102 223 Section 8.2 # TS 102 223 Section 8.2
class AlphaIdentifier(COMPR_TLV_IE, tag=0x85): class AlphaIdentifier(COMPR_TLV_IE, tag=0x85):
# FIXME: like EF.ADN _construct = GsmOrUcs2Adapter(GreedyBytes)
pass
# TS 102 223 Section 8.3 # TS 102 223 Section 8.3
class Subaddress(COMPR_TLV_IE, tag=0x88): class Subaddress(COMPR_TLV_IE, tag=0x88):
+9 -2
View File
@@ -411,14 +411,21 @@ class JsonHttpApiClient():
# SGP.22, section 6.5.1) # SGP.22, section 6.5.1)
if response.status_code != self.api_func.expected_http_status: if response.status_code != self.api_func.expected_http_status:
raise HttpStatusError(response) raise HttpStatusError(response)
if response.content and not response.headers.get('Content-Type').startswith(req_headers['Content-Type']): resp_content_type = response.headers.get('Content-Type')
if response.content and not resp_content_type.startswith(req_headers['Content-Type']):
raise HttpHeaderError(response) raise HttpHeaderError(response)
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'): if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
raise HttpHeaderError(response) raise HttpHeaderError(response)
# Decode response and return the result back to the caller # Decode response and return the result back to the caller
if response.content: if response.content:
output = self.api_func.decode_client(response.json()) if resp_content_type.startswith('application/json'):
output = self.api_func.decode_client(response.json())
elif resp_content_type.startswith('text/plain;charset=UTF-8'):
output = { 'data': response.content.decode('utf-8') }
else:
raise HttpHeaderError(f'unimplemented response Content-Type: {response.headers=!r}')
# In case the response contains a header, check it to make sure that the API call was executed successfully # In case the response contains a header, check it to make sure that the API call was executed successfully
# (the presence of the header field is checked by the decode_client method) # (the presence of the header field is checked by the decode_client method)
if 'header' in output: if 'header' in output:
+9 -2
View File
@@ -1517,8 +1517,11 @@ class ProfileElementHeader(ProfileElement):
def mandatory_service_add(self, service_name): def mandatory_service_add(self, service_name):
self.decoded['eUICC-Mandatory-services'][service_name] = None self.decoded['eUICC-Mandatory-services'][service_name] = None
def mandatory_service_present(self, service_name):
return service_name in self.decoded['eUICC-Mandatory-services'].keys()
def mandatory_service_remove(self, service_name): def mandatory_service_remove(self, service_name):
if service_name in self.decoded['eUICC-Mandatory-services'].keys(): if self.mandatory_service_present(service_name):
del self.decoded['eUICC-Mandatory-services'][service_name] del self.decoded['eUICC-Mandatory-services'][service_name]
else: else:
raise ValueError("service not in eUICC-Mandatory-services list, cannot remove") raise ValueError("service not in eUICC-Mandatory-services list, cannot remove")
@@ -1774,9 +1777,13 @@ class ProfileElementSequence:
hdr_pe = self.get_pe_for_type('header') hdr_pe = self.get_pe_for_type('header')
# patch in the 'manual' services from the existing list: # patch in the 'manual' services from the existing list:
old_svc_set = set()
for old_svc in hdr_pe.decoded['eUICC-Mandatory-services'].keys(): for old_svc in hdr_pe.decoded['eUICC-Mandatory-services'].keys():
if old_svc in manual_services: if old_svc in manual_services:
svc_set.add(old_svc) old_svc_set.add(old_svc)
logger.debug(f"{svc_set=} + {old_svc_set=}")
svc_set = svc_set.union(old_svc_set)
logger.debug(f"{svc_set=}")
hdr_pe.decoded['eUICC-Mandatory-services'] = {x: None for x in svc_set} hdr_pe.decoded['eUICC-Mandatory-services'] = {x: None for x in svc_set}
def rebuild_mandatory_gfstelist(self): def rebuild_mandatory_gfstelist(self):
+55 -3
View File
@@ -20,6 +20,9 @@
import copy import copy
import pprint import pprint
import logging
import traceback
import inspect
from typing import Generator, Union from typing import Generator, Union
from pySim.esim.saip.personalization import ConfigurableParameter from pySim.esim.saip.personalization import ConfigurableParameter
from pySim.esim.saip import param_source from pySim.esim.saip import param_source
@@ -27,6 +30,10 @@ from pySim.esim.saip import ProfileElementSequence, ProfileElementSD
from pySim.global_platform import KeyUsageQualifier from pySim.global_platform import KeyUsageQualifier
from osmocom.utils import b2h from osmocom.utils import b2h
logger = logging.getLogger(__name__)
def _func_():
return inspect.currentframe().f_back.f_code.co_name
# a list of ConfigurableParameter classes and/or ConfigurableParameter class instances # a list of ConfigurableParameter classes and/or ConfigurableParameter class instances
ParamList = list[Union[type[ConfigurableParameter], ConfigurableParameter]] ParamList = list[Union[type[ConfigurableParameter], ConfigurableParameter]]
@@ -121,6 +128,8 @@ class BatchPersonalization:
value = p.param_cls.validate_val(input_value) value = p.param_cls.validate_val(input_value)
p.param_cls.apply_val(pes, value) p.param_cls.apply_val(pes, value)
except Exception as e: except Exception as e:
print(traceback.format_exc())
logger.error('during %s: %r', _func_(), e)
raise ValueError(f'{p.param_cls.get_name()} fed by {p.src.name}: {e}') from e raise ValueError(f'{p.param_cls.get_name()} fed by {p.src.name}: {e}') from e
pes.rebuild_mandatory_services() pes.rebuild_mandatory_services()
@@ -268,6 +277,34 @@ class BatchAudit(list):
Usage example: Usage example:
MY_COLUMN_ORDER = (
'ICCID',
'IMSI',
'K',
'OPc',
'PIN1',
'PIN2',
'PUK1',
'PUK2',
'ADM1',
'ADM2',
)
MY_COLUMN_BLACKLIST = (
'MNC-LEN',
'5G-SUCI-CalcInfo',
'5G-SUCI-active',
'5G-SUCI-in-USIM',
)
def my_sort_key(column):
try:
i = MY_COLUMN_ORDER.index(column)
except ValueError:
# place unlisted items last
i = len(MY_COLUMN_ORDER)
return (i, column)
ba = BatchAudit(params=(personalization.Iccid, )) ba = BatchAudit(params=(personalization.Iccid, ))
for upp_der in upps: for upp_der in upps:
ba.add_audit(upp_der) ba.add_audit(upp_der)
@@ -275,7 +312,7 @@ class BatchAudit(list):
with open('output.csv', 'wb') as csv_data: with open('output.csv', 'wb') as csv_data:
csv_str = io.TextIOWrapper(csv_data, 'utf-8', newline='') csv_str = io.TextIOWrapper(csv_data, 'utf-8', newline='')
csv.writer(csv_str).writerows( ba.to_csv_rows() ) csv.writer(csv_str).writerows( ba.to_csv_rows(sort_key=my_sort_key, column_blacklist=MY_COLUMN_BLACKLIST) )
csv_str.flush() csv_str.flush()
BatchAudit itself is a list, callers may use the standard python list API to access the UppAudit instances. BatchAudit itself is a list, callers may use the standard python list API to access the UppAudit instances.
@@ -324,12 +361,27 @@ class BatchAudit(list):
return batch_audit return batch_audit
def to_csv_rows(self, headers=True, sort_key=None): def to_csv_rows(self, headers=True, sort_key=None, column_blacklist=None):
"""generator that yields all audits' values as rows, useful feed to a csv.writer.""" """generator that yields all audits' values as rows, useful feed to a csv.writer.
headers: when True, add a first row listing the parameter names, as column headers.
sort_key: key function to determine the ordering of columns, so that it can be used with
sorted(key=sort_key). sort_key(column_name) should return a comparable value. column_name is a str as
returned by a ConfigurableParameter.get_name() implementation.
For a usage example, see the doc for class BatchAudit.
column_blacklist: parameters to exclude from the CSV export. Pass an iterable of str, each str reflecting a
column name as returned by a ConfigurableParameter.get_name() implementation.
column_blacklist looks just like MY_COLUMN_ORDER in the usage example found in the doc for class BatchAudit.
"""
columns = set() columns = set()
for audit in self: for audit in self:
columns.update(audit.keys()) columns.update(audit.keys())
if column_blacklist:
columns.difference_update(set(column_blacklist))
columns = tuple(sorted(columns, key=sort_key)) columns = tuple(sorted(columns, key=sort_key))
if headers: if headers:
+545 -103
View File
@@ -19,19 +19,26 @@ import abc
import enum import enum
import io import io
import re import re
import json
from typing import List, Tuple, Generator, Optional from typing import List, Tuple, Generator, Optional
from construct.core import StreamError from construct.core import StreamError
from osmocom.tlv import camel_to_snake from osmocom.tlv import camel_to_snake
from osmocom.utils import hexstr from osmocom.utils import hexstr
from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, rpad, sanitize_iccid from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, rpad, sanitize_iccid
from pySim.ts_31_102 import EF_AD from pySim.ts_31_102 import EF_AD, EF_UST, EF_Routing_Indicator, EF_SUCI_Calc_Info, DF_USIM_5GS
from pySim.ts_51_011 import EF_SMSP from pySim.ts_51_011 import EF_SMSP
from pySim.esim.saip import param_source from pySim.esim.saip import param_source
from pySim.esim.saip import ProfileElement, ProfileElementSD, ProfileElementSequence from pySim.esim.saip import ProfileElement, ProfileElementSD, ProfileElementSequence
from pySim.esim.saip import ProfileElementHeader
from pySim.esim.saip import SecurityDomainKey, SecurityDomainKeyComponent from pySim.esim.saip import SecurityDomainKey, SecurityDomainKeyComponent
from pySim.global_platform import KeyUsageQualifier, KeyType from pySim.global_platform import KeyUsageQualifier, KeyType
# optimization: instantiate class instance to get the fid only once.
file_path_df_5gs = bytes.fromhex(DF_USIM_5GS().fid)
fid_ri = bytes.fromhex(EF_Routing_Indicator().fid)
fid_sucici = bytes.fromhex(EF_SUCI_Calc_Info().fid)
def unrpad(s: hexstr, c='f') -> hexstr: def unrpad(s: hexstr, c='f') -> hexstr:
return hexstr(s.rstrip(c)) return hexstr(s.rstrip(c))
@@ -226,7 +233,7 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
"""Same as get_values_from_pes() but expecting a single value. """Same as get_values_from_pes() but expecting a single value.
get_values_from_pes() may return values like this: get_values_from_pes() may return values like this:
[{ 'AlgorithmID': 'Milenage' }, { 'AlgorithmID': 'Milenage' }] [{ 'AlgorithmID': 'Milenage' }, { 'AlgorithmID': 'Milenage' }]
This ensures that all these entries are identical and would return only Instead, ensure that all these entries are identical and would return only
{ 'AlgorithmID': 'Milenage' }. { 'AlgorithmID': 'Milenage' }.
This is relevant for any profile element that may appear multiple times in the same PES (only a few), This is relevant for any profile element that may appear multiple times in the same PES (only a few),
@@ -290,7 +297,9 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
May be overridden by subclasses. May be overridden by subclasses.
This default implementation returns the maximum allowed value length -- a good fit for most subclasses. This default implementation returns the maximum allowed value length -- a good fit for most subclasses.
''' '''
return cls.get_len_range()[1] or 16 l = cls.get_len_range()[1] or 16
l = min(10*80, l)
return l
@classmethod @classmethod
def is_super_of(cls, other_class): def is_super_of(cls, other_class):
@@ -420,68 +429,70 @@ class BinaryParam(ConfigurableParameter):
class EnumParam(ConfigurableParameter): class EnumParam(ConfigurableParameter):
"""ConfigurableParameter for named integer enumeration values. """ConfigurableParameter for named value enumerations.
Subclasses must define a nested enum.IntEnum named 'Values' listing all valid names and their Subclasses define an own value_map, and implement their own apply_val() and get_values_from_pes().
integer codes. apply_val() and get_values_from_pes() are not implemented here and this must """
be inherited from another mixin.""" value_map = {
# For example:
class Values(enum.IntEnum): #'Meaningful label for value 23': 0x23,
pass # subclasses override this # Where 0x23 is a valid value to use for apply_val(), of any valid type.
}
_value_map_reverse = None
@classmethod @classmethod
def validate_val(cls, val) -> int: def validate_val(cls, val):
if isinstance(val, int): orig_val = val
try: enum_val = None
return int(cls.Values(val)) if isinstance(val, str):
except ValueError: enum_name = val
pass enum_val = cls.map_name_to_val(enum_name)
elif isinstance(val, str):
member = cls.map_name_to_val(val, strict=False)
if member is not None:
return member
valid = ', '.join(m.name for m in cls.Values) # if the str is not one of the known value_map.keys(), is it maybe one of value_map.keys()?
raise ValueError(f"{cls.get_name()}: invalid argument: {val!r}. Valid arguments are: {valid}") if enum_val is None and val in cls.value_map.values():
enum_val = val
if enum_val not in cls.value_map.values():
raise ValueError(f"{cls.get_name()}: invalid argument: {orig_val!r}. Valid arguments are:"
f" {', '.join(cls.value_map.keys())}")
return enum_val
@classmethod @classmethod
def map_name_to_val(cls, name: str, strict=True) -> int: def map_name_to_val(cls, name:str, strict=True):
"""Return the integer value for a given enum member name. Performs an exact match first, val = cls.value_map.get(name)
then falls back to fuzzy matching (case-insensitive, punctuation-insensitive).""" if val is not None:
try: return val
return int(cls.Values[name])
except KeyError:
pass
clean = cls.clean_name_str(name) clean_name = cls.clean_name_str(name)
for member in cls.Values: for k, v in cls.value_map.items():
if cls.clean_name_str(member.name) == clean: if clean_name == cls.clean_name_str(k):
return int(member) return v
if strict: if strict:
valid = ', '.join(m.name for m in cls.Values) raise ValueError(f"Problem in {cls.get_name()}: {name!r} is not a known value."
raise ValueError(f"{cls.get_name()}: {name!r} is not a known value. Known values are: {valid}") f" Known values are: {cls.value_map.keys()!r}")
return None return None
@classmethod @classmethod
def map_val_to_name(cls, val, strict=False) -> str: def map_val_to_name(cls, val, strict=False) -> str:
"""Return the enum member name for a given integer value.""" if cls._value_map_reverse is None:
try: cls._value_map_reverse = dict((v, k) for k, v in cls.value_map.items())
return cls.Values(val).name
except ValueError: name = cls._value_map_reverse.get(val)
if strict: if name:
raise ValueError(f"{cls.get_name()}: {val!r} ({type(val).__name__}) is not a known value.") return name
return None if strict:
raise ValueError(f"Problem in {cls.get_name()}: {val!r} ({type(val)}) is not a known value."
f" Known values are: {cls.value_map.values()!r}")
return None
@classmethod @classmethod
def name_normalize(cls, name: str) -> str: def name_normalize(cls, name:str) -> str:
"""Map a (possibly fuzzy) name to its canonical enum member name.""" return cls.map_val_to_name(cls.map_name_to_val(name))
return cls.Values(cls.map_name_to_val(name)).name
@classmethod @classmethod
def clean_name_str(cls, val: str) -> str: def clean_name_str(cls, val):
"""Strip punctuation and case for fuzzy name comparison.
Treats hyphens and underscores as equivalent (both removed)."""
return re.sub('[^0-9A-Za-z]', '', val).lower() return re.sub('[^0-9A-Za-z]', '', val).lower()
@@ -635,13 +646,14 @@ class SmspTpScAddr(ConfigurableParameter):
# - To generate the right amount of fillFileContent, pass total_len=42 to encode_record_bin(). # - To generate the right amount of fillFileContent, pass total_len=42 to encode_record_bin().
# - To show the right size in the PES, set f_smsp.rec_len = 42 # - To show the right size in the PES, set f_smsp.rec_len = 42
ef_smsp_dec['alpha_id'] = '' ef_smsp_dec['alpha_id'] = ''
f_smsp.rec_len = 42
# we can set this to choose a fixed length:
# f_smsp.rec_len = 42
# but leave rec_len unchanged to keep the same length as was found in the eSIM template.
# re-encode into the File body. # re-encode into the File body.
#
#print("SMSP (new): %s" % f_smsp.body)
# re-generate the pe.decoded member from the File instance
f_smsp.body = ef_smsp.encode_record_bin(ef_smsp_dec, 1, total_len=f_smsp.rec_len) f_smsp.body = ef_smsp.encode_record_bin(ef_smsp_dec, 1, total_len=f_smsp.rec_len)
# re-generate the pe.decoded member from the File instance
pe.file2pe(f_smsp) pe.file2pe(f_smsp)
@classmethod @classmethod
@@ -663,69 +675,57 @@ class SmspTpScAddr(ConfigurableParameter):
class MncLen(EnumParam): class MncLen(EnumParam):
"""MNC length. Sets only the MNC length field in EF.AD (Administrative Data). """MNC length. Must be either 2 or 3. Sets only the MNC length field in EF-AD (Administrative Data)."""
Accepted values: integer 2 or 3, digit strings '2' or '3', or enum names 'MNC2'/'MNC3'.
"""
name = 'MNC-LEN' name = 'MNC-LEN'
example_input = '2' value_map = { '2': 2, '3': 3 }
default_source = param_source.ConstantSource default_source = param_source.ConstantSource
example_input = '2'
class Values(enum.IntEnum):
MNC2 = 2
MNC3 = 3
@classmethod @classmethod
def validate_val(cls, val): def apply_val(cls, pes: ProfileElementSequence, val):
if isinstance(val, str) and val.isdigit(): """val must be an int: either 2 or 3"""
val = int(val)
return super().validate_val(val)
@classmethod
def _get_f_ad(cls, pe: ProfileElement):
if not hasattr(pe, 'files'):
return None
f_ad = pe.files.get('ef-ad', None)
if f_ad and f_ad.body:
return f_ad
return None
@classmethod
def _decode_f_ad(cls, f_ad):
try:
ef_ad_dec = EF_AD().decode_bin(f_ad.body)
except StreamError:
return None
if 'mnc_len' not in ef_ad_dec:
return None
return ef_ad_dec
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val: int):
for pe in pes.get_pes_for_type('usim'): for pe in pes.get_pes_for_type('usim'):
f_ad = cls._get_f_ad(pe) if not hasattr(pe, 'files'):
if f_ad is None: continue
f_ad = pe.files.get('ef-ad')
if not f_ad:
continue continue
# decode existing values # decode existing values
ef_ad_dec = cls._decode_f_ad(f_ad) if not f_ad.body:
if ef_ad_dec is None: continue
try:
ef_ad = EF_AD()
ef_ad_dec = ef_ad.decode_bin(f_ad.body)
except StreamError:
continue
if 'mnc_len' not in ef_ad_dec:
continue continue
# change mnc_len # change mnc_len
ef_ad_dec['mnc_len'] = val ef_ad_dec['mnc_len'] = val
# re-encode into the File body # re-encode into the File body
f_ad.body = EF_AD().encode_bin(ef_ad_dec) f_ad.body = ef_ad.encode_bin(ef_ad_dec)
pe.file2pe(f_ad) pe.file2pe(f_ad)
@classmethod @classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence): def get_values_from_pes(cls, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('usim'): for pe in pes.get_pes_for_type('usim'):
f_ad = cls._get_f_ad(pe) if not hasattr(pe, 'files'):
continue
f_ad = pe.files.get('ef-ad', None)
if f_ad is None: if f_ad is None:
continue continue
ef_ad_dec = cls._decode_f_ad(f_ad)
if ef_ad_dec is None: try:
ef_ad = EF_AD()
ef_ad_dec = ef_ad.decode_bin(f_ad.body)
except StreamError:
continue continue
mnc_len = ef_ad_dec.get('mnc_len')
yield { cls.name: str(mnc_len) } mnc_len = ef_ad_dec.get('mnc_len', None)
if mnc_len is None:
continue
yield { cls.name: cls.map_val_to_name(int(mnc_len)) }
class SdKey(BinaryParam): class SdKey(BinaryParam):
@@ -1099,17 +1099,17 @@ class AlgorithmID(EnumParam, AlgoConfig):
"""use validate_val() from EnumParam, and apply_val() from AlgoConfig. """use validate_val() from EnumParam, and apply_val() from AlgoConfig.
In get_values_from_pes(), return enum value names, not raw values.""" In get_values_from_pes(), return enum value names, not raw values."""
name = "Algorithm" name = "Algorithm"
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
value_map = {
"Milenage" : 1,
"TUAK" : 2,
"usim-test" : 3,
}
algo_config_key = 'algorithmID' algo_config_key = 'algorithmID'
example_input = "Milenage" example_input = "Milenage"
default_source = param_source.ConstantSource default_source = param_source.ConstantSource
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn # EnumParam.validate_val() returns the int values from value_map
class Values(enum.IntEnum):
Milenage = 1
TUAK = 2
usim_test = 3 # input 'usim-test' also accepted via fuzzy matching
# EnumParam.validate_val() returns the int values from Values
@classmethod @classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence): def get_values_from_pes(cls, pes: ProfileElementSequence):
@@ -1184,3 +1184,445 @@ class TuakNumberOfKeccak(IntegerParam, AlgoConfig):
max_val = 255 max_val = 255
example_input = '1' example_input = '1'
default_source = param_source.ConstantSource default_source = param_source.ConstantSource
numeric_base = None # indicate that this won't need random number sources
class EfUstServiceParam(EnumParam):
"""superclass for EF-UST service flag parameters"""
service_idx = 0
value_map = { 'enabled': True, 'disabled': False }
default_source = param_source.ConstantSource
example_input = sorted(value_map.keys())[0]
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
for pe in pes.get_pes_for_type('usim'):
f_ust = pe.files['ef-ust']
ef_ust = EF_UST()
ust = ef_ust.decode_bin(f_ust.body)
ust[cls.service_idx]['activated'] = val
f_ust.body = ef_ust.encode_bin(ust)
pe.file2pe(f_ust)
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('usim'):
f_ust = pe.files.get('ef-ust', None)
if not f_ust:
continue
ef_ust = EF_UST()
try:
ust = ef_ust.decode_bin(f_ust.body)
service_flag = ust[cls.service_idx]['activated']
yield { cls.name: cls.map_val_to_name(service_flag) }
except:
pass
class SuciActive(EfUstServiceParam):
"""EF-UST service nr 124: enable or disable the SUCI service."""
service_idx = 124
name = '5G-SUCI-active'
value_map = { 'SUCI-off': False, 'SUCI-on': True }
example_input = 'SUCI-on'
class SuciInUsim(EfUstServiceParam):
"""EF-UST service nr 125: calculate SUCI in UE or in USIM"""
service_idx = 125
name = '5G-SUCI-in-USIM'
value_map = { 'SUCI-in-UE': False, 'SUCI-in-USIM': True }
example_input = 'SUCI-in-USIM'
class SuciRi(ConfigurableParameter):
"""SUCI Routing Indicator as in section 4.4.11.11 of 3GPP TS 31.102"""
name = '5G-SUCI-RI'
allow_chars = '0123456789'
min_len = 1
max_len = 4
allow_types = (str,)
example_input = '0'
default_source = param_source.ConstantSource
KEY_RI = "routing_indicator"
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
for pe in pes.get_pes_for_type('df-5gs'):
f_ri = pe.files.get('ef-routing-indicator', None)
if f_ri is None:
continue
ef_ri = EF_Routing_Indicator()
ri = ef_ri.decode_bin(f_ri.body)
ri[cls.KEY_RI] = str(val)
f_ri.body = ef_ri.encode_bin(ri)
pe.file2pe(f_ri)
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('df-5gs'):
f_ri = pe.files.get('ef-routing-indicator', None)
if f_ri is None:
continue
ef_ri = EF_Routing_Indicator()
try:
ri = ef_ri.decode_bin(f_ri.body)
yield { cls.name: ri.get(cls.KEY_RI) }
except:
pass
class SuciCalcInfoParameter(ConfigurableParameter):
"""SUCI Calculation Information as in section 4.4.11.8 of 3GPP TS 31.102"""
name = '5G-SUCI-CalcInfo'
default_source = param_source.ConstantSource
allow_types = (str,)
max_len = 4096 # to indicate a large input field to UI renderers
example_input = '{"prot_scheme_id_list": [], "hnet_pubkey_list": []}'
PE_IN_UE = ("df-5gs", "ef-suci-calc-info")
PE_IN_USIM = ("df-saip", "ef-suci-calc-info-usim")
suci_calc_info_pe = None
@classmethod
def validate_val(cls, val):
val = super().validate_val(val)
if not val:
val = "{}"
# check that it is a dict something like
# {
# "prot_scheme_id_list": [
# {"priority": 0, "identifier": 2, "key_index": 1},
# {"priority": 1, "identifier": 1, "key_index": 2},
# ],
# "hnet_pubkey_list": [
# {"hnet_pubkey_identifier": 27,
# "hnet_pubkey": "0472DA71976234CE833A6907425867B82E074D44EF907DFB4B3E21C1C2256EBCD15A7DED52FCBB097A4ED250E036C7B9C8C7004C4EEDC4F068CD7BF8D3F900E3B4"},
# {"hnet_pubkey_identifier": 30,
# "hnet_pubkey": "5A8D38864820197C3394B92613B20B91633CBD897119273BF8E4A6F4EEC0A650"},
# ],
# }
try:
d = json.loads(val)
except json.decoder.JSONDecodeError as e:
raise ValueError(f"Cannot parse SUCI Calc Info: {e}") from e
KEY_PSI_LIST = 'prot_scheme_id_list'
KEY_HPK_LIST = 'hnet_pubkey_list'
KEYS_D = set((KEY_HPK_LIST, KEY_PSI_LIST))
KEYS_PSI = set(('identifier', 'key_index', 'priority'))
KEYS_HPK = set(('hnet_pubkey_identifier', 'hnet_pubkey'))
if not d:
d = { KEY_PSI_LIST: [], KEY_HPK_LIST: [] }
if not (isinstance(d, dict)
and set(d.keys()) == KEYS_D):
raise ValueError(f"Unexpected structure in SUCI Calc Info: expected dict with entries {KEYS_D}")
psi = d.get(KEY_PSI_LIST, None)
if not all((set(e.keys()) == KEYS_PSI) for e in psi):
raise ValueError("Unexpected structure in SUCI Calc Info:"
f" in {KEY_PSI_LIST}, expected dict with entries {KEYS_PSI}")
hpk = d.get(KEY_HPK_LIST, None)
if not all((set(e.keys()) == KEYS_HPK) for e in hpk):
raise ValueError("Unexpected structure in SUCI Calc Info:"
f" in {KEY_HPK_LIST}, expected dict with entries {KEYS_HPK}")
return d
@classmethod
def _apply_suci(cls, pes: ProfileElementSequence, val, pe_type="df-5gs", pe_file="ef-suci-calc-info"):
for pe in pes.get_pes_for_type(pe_type):
f_sucici = pe.files.get(pe_file, None)
if not f_sucici:
continue
ef_sucici = EF_SUCI_Calc_Info()
body = ef_sucici.encode_bin(val)
# 0xff pad up to the existing file size, so that the underlying template doesn't come through
is_size = f_sucici.file_size
pad_n = is_size - len(body)
if pad_n > 0:
body = body + b'\xff' * pad_n
f_sucici.body = body
pe.file2pe(f_sucici)
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
cls._apply_suci(pes, val, *cls.suci_calc_info_pe)
@staticmethod
def normalize_sucici(sucici:dict):
"""Normalize the CalcInfo dict so it can be json encoded:
convert bytes to hex strings."""
if not sucici:
sucici = {}
for hnet_pubkey in sucici.get('hnet_pubkey_list', ()):
val = hnet_pubkey['hnet_pubkey']
if isinstance(val, bytes):
val = b2h(val)
hnet_pubkey['hnet_pubkey'] = val
return sucici
@classmethod
def _get_suci(cls, pes: ProfileElementSequence, pe_type="df-5gs", pe_file="ef-suci-calc-info"):
for pe in pes.get_pes_for_type(pe_type):
f_sucici = pe.files.get(pe_file, None)
if not f_sucici:
continue
ef_sucici = EF_SUCI_Calc_Info()
sucici = ef_sucici.decode_bin(f_sucici.body)
# normalize to string (bytes cannot go into json)
sucici = cls.normalize_sucici(sucici)
yield { cls.name: json.dumps(sucici) }
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
yield from cls._get_suci(pes, *cls.suci_calc_info_pe)
class SuciCalcInfoUe(SuciCalcInfoParameter):
"""SUCI Calculation Information as in section 4.4.11.8 of 3GPP TS 31.102, readable by UE (DF-5GS)"""
name = '5G-SUCI-CalcInfo-UE'
suci_calc_info_pe = SuciCalcInfoParameter.PE_IN_UE
class SuciCalcInfoUsim(SuciCalcInfoParameter):
"""SUCI Calculation Information as in section 4.4.11.8 of 3GPP TS 31.102, readable only by USIM (DF-SAIP)"""
name = '5G-SUCI-CalcInfo-USIM'
suci_calc_info_pe = SuciCalcInfoParameter.PE_IN_USIM
def gfm_find(pes: ProfileElementSequence, file_path:bytes, ef_fid:bytes):
"""look through genericFileManagement PE and return the fmc list with start and end indexes as
(fmc_list, first_idx, after_last_idx)
so that fmc_list[first_idx:after_last_idx] is the slice of file management commands relevant to the given
file_path/ef_fid.
"""
for pe in pes.get_pes_for_type('genericFileManagement'):
path_match = False
creating_fid = False
for fmc in pe.decoded['fileManagementCMD']:
first = None
last = None
for idx in range(len(fmc)):
cmd, arg = fmc[idx]
if cmd == 'filePath':
path_match = (arg == file_path)
if not path_match:
creating_fid = False
elif path_match and cmd == 'createFCP':
creating_fid = (arg.get('fileID') == ef_fid)
if creating_fid:
if first is None:
first = idx
last = idx
first = min(first, idx)
last = max(last, idx)
if first is not None:
yield fmc, first, last + 1
# genericFileManagement 5G params
def pes_get_adf_fid(pes:ProfileElementSequence, naa_name="usim", adf_name="adf-usim"):
adf = pes.get_pe_for_type(naa_name)
return adf.decoded[adf_name][0][1]['fileID']
def mk_adf_df_path(pes, naa:str, adf:str, file_path:bytes) -> bytes:
adf_file_id = pes_get_adf_fid(pes, naa, adf)
return b''.join((adf_file_id, file_path))
def gfm_get_file_content(pes: ProfileElementSequence, naa:str, adf:str, file_path:bytes, ef_fid:bytes) -> bytes:
'''find a given file in the genericFileManagement section, and return the bytes from the first fillFileContent
item.
TODO: implement File.from_gfm() and return the full resulting bytes?
'''
adf_df_path = mk_adf_df_path(pes, naa, adf, file_path)
data = []
for fmc, first_idx, after_last_idx in gfm_find(pes, adf_df_path, ef_fid):
assert fmc[first_idx][0] == 'createFCP'
assert after_last_idx > first_idx
idx = first_idx + 1
while idx < after_last_idx:
if fmc[idx][0] == 'fillFileContent':
data.append(fmc[idx][1])
idx += 1
return data
def gfm_set_file_content(pes: ProfileElementSequence, naa:str, adf:str, file_path:bytes, ef_fid:bytes, file_content:bytes) -> int:
adf_df_path = mk_adf_df_path(pes, naa, adf, file_path)
found = 0
for fmc, first_idx, after_last_idx in gfm_find(pes, adf_df_path, ef_fid):
assert fmc[first_idx][0] == 'createFCP'
assert after_last_idx > first_idx
new_fmc = [
fmc[first_idx],
('fillFileContent', file_content),
]
new_fmc[0][1]['efFileSize'] = bytes((len(file_content), ))
fmc[first_idx:after_last_idx] = new_fmc
found += 1
return found
class GfmSuciRi(SuciRi):
"""SUCI Routing Indicator as in section 4.4.11.11 of 3GPP TS 31.102,
applied via General File Management. Intended for SAIP 2.1 profiles."""
name = 'GFM-5G-SUCI-RI'
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
ri = {
"routing_indicator": str(val),
"rfu": "ffff"
}
ef_ri = EF_Routing_Indicator()
found = gfm_set_file_content(pes, 'usim', 'adf-usim', file_path_df_5gs, fid_ri,
ef_ri.encode_bin(ri))
if not found:
raise ValueError(f"No target file found, Cannot apply {cls.name} = {ri}")
data = gfm_get_file_content(pes, 'usim', 'adf-usim', file_path_df_5gs, fid_ri)
val = ef_ri.decode_bin(b''.join(data))
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
data = gfm_get_file_content(pes, 'usim', 'adf-usim', file_path_df_5gs, fid_ri)
if not data:
return
data = b''.join(data)
if not data:
return
ef_ri = EF_Routing_Indicator()
ri = ef_ri.decode_bin(data)
yield { cls.name: ri.get(cls.KEY_RI) }
class GfmSuciCalcInfoUe(SuciCalcInfoUe):
"""SUCI Calculation Information as in section 4.4.11.8 of 3GPP TS 31.102, readable by UE (DF-5GS),
applied via General File Management. Intended for SAIP 2.1 profiles."""
name = 'GFM-5G-SUCI-CalcInfo-UE'
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
if not isinstance(val, dict):
raise ValueError("val should be a dict, after 'val = SuciCalcInfoParameter.validate_val(val)'")
ef_sucici = EF_SUCI_Calc_Info()
body = ef_sucici.encode_bin(val)
gfm_set_file_content(pes, 'usim', 'adf-usim', file_path_df_5gs, fid_sucici,
body)
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
data = gfm_get_file_content(pes, 'usim', 'adf-usim', file_path_df_5gs, fid_sucici)
if not data:
return
data = b''.join(data)
if not data:
return
ef_sucici = EF_SUCI_Calc_Info()
sucici = ef_sucici.decode_bin(data)
sucici = cls.normalize_sucici(sucici)
yield { cls.name: json.dumps(sucici) }
class EuiccMandatoryServiceParam(EnumParam):
"""superclass for managing items of the ProfileHeader / eUICC-Mandatory-services ServicesList"""
service_name = None
value_map = { 'mandatory': True, 'optional': False }
default_source = param_source.ConstantSource
example_input = sorted(value_map.keys())[0]
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
for pe in pes.get_pes_for_type('header'):
assert isinstance(pe, ProfileElementHeader)
if val:
pe.mandatory_service_add(cls.service_name)
else:
# explicitly check to avoid exception when the service is already not-present
if pe.mandatory_service_present(cls.service_name):
pe.mandatory_service_remove(cls.service_name)
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('header'):
assert isinstance(pe, ProfileElementHeader)
val = pe.mandatory_service_present(cls.service_name)
yield { cls.name: cls.map_val_to_name(val) }
class EuiccMandatoryServiceGetIdentity(EuiccMandatoryServiceParam):
"""eUICC Mandatory Services: get-identity. The eUICC must be capable of providing a 5G identity using SUCI-CalcInfo
located in the USIM's DF-SAIP, see parameter 5G-SUCI-CalcInfo-USIM."""
name = '5G-eUICC-get-identity'
service_name = 'get-identity'
class EuiccMandatoryServiceProfileA(EuiccMandatoryServiceParam):
"""eUICC Mandatory Services: profile-a-x25519. The eUICC must be able to estblish a 5G identity using an X25519 key,
as provided in a profile-A ("identifier": 1) key in SUCI-CalcInfo located in the USIM's DF-SAIP, see parameter
5G-SUCI-CalcInfo-USIM."""
name = '5G-eUICC-profile-a-x25519'
service_name = 'profile-a-x25519'
class EuiccMandatoryServiceProfileB(EuiccMandatoryServiceParam):
"""eUICC Mandatory Services: profile-b-p256. The eUICC must be able to estblish a 5G identity using a P256 key, as
provided in a profile-B ("identifier": 2) key in SUCI-CalcInfo located in the USIM's DF-SAIP, see parameter
5G-SUCI-CalcInfo-USIM."""
name = '5G-eUICC-profile-b-p256'
service_name = 'profile-b-p256'
class Gid1(BinaryParam):
"""Group Identifier Level 1 (3GPP TS 31.102 4.2.10), an arbitrary binary field defined by the operator."""
name = 'GID1'
gid_ef_name = 'ef-gid1'
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
# patch OPT-USIM/EF.GIDn
file_replace_content(pes.get_pe_for_type('opt-usim').decoded[cls.gid_ef_name], h2b(val))
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('opt-usim'):
gid_f = pe.files.get(cls.gid_ef_name, None)
if gid_f is not None:
yield { cls.name: b2h(gid_f.body) }
class Gid2(Gid1):
"""Group Identifier Level 2 (3GPP TS 31.102 4.2.11), an arbitrary binary field defined by the operator."""
name = 'GID2'
gid_ef_name = 'ef-gid2'
class Gid1Active(EfUstServiceParam):
"""EF-UST service nr 17: enable or disable GID1 (Group Identifier Level 1)."""
service_idx = 17
name = 'GID1-active'
value_map = { 'inactive': False, 'active': True }
example_input = 'inactive'
class Gid2Active(Gid1Active):
"""EF-UST service nr 18: enable or disable GID2 (Group Identifier Level 2)."""
service_idx = 18
name = 'GID2-active'
Binary file not shown.
+131 -21
View File
@@ -21,6 +21,7 @@ import enum
import io import io
import sys import sys
import unittest import unittest
import json
from importlib import resources from importlib import resources
from osmocom.utils import hexstr from osmocom.utils import hexstr
from pySim.esim.saip import ProfileElementSequence from pySim.esim.saip import ProfileElementSequence
@@ -53,16 +54,21 @@ class ConfigurableParameterTest(unittest.TestCase):
def test_parameters(self): def test_parameters(self):
upp_fnames = ( upp_fnames = (
'SAIP2.1_gfmsuci.der',
'TS48v5_SAIP2.1A_NoBERTLV.der', 'TS48v5_SAIP2.1A_NoBERTLV.der',
'TS48v5_SAIP2.3_BERTLV_SUCI.der', 'TS48v5_SAIP2.3_BERTLV_SUCI.der',
) )
class Paramtest: class Paramtest:
def __init__(self, param_cls, val, expect_val, expect_clean_val=None): iff_present_default = False
def __init__(self, param_cls, val, expect_val, expect_clean_val=None, iff_present=None):
self.param_cls = param_cls self.param_cls = param_cls
self.val = val self.val = val
self.expect_clean_val = expect_clean_val self.expect_clean_val = expect_clean_val
self.expect_val = expect_val self.expect_val = expect_val
if iff_present is None:
iff_present = Paramtest.iff_present_default
self.iff_present = iff_present
param_tests = [ param_tests = [
Paramtest(param_cls=p13n.Imsi, val='123456', Paramtest(param_cls=p13n.Imsi, val='123456',
@@ -148,7 +154,7 @@ class ConfigurableParameterTest(unittest.TestCase):
Paramtest(param_cls=p13n.AlgorithmID, Paramtest(param_cls=p13n.AlgorithmID,
val='usim-test', val='usim-test',
expect_clean_val=3, expect_clean_val=3,
expect_val='usim_test'), expect_val='usim-test'),
Paramtest(param_cls=p13n.AlgorithmID, Paramtest(param_cls=p13n.AlgorithmID,
val=1, val=1,
@@ -161,7 +167,7 @@ class ConfigurableParameterTest(unittest.TestCase):
Paramtest(param_cls=p13n.AlgorithmID, Paramtest(param_cls=p13n.AlgorithmID,
val=3, val=3,
expect_clean_val=3, expect_clean_val=3,
expect_val='usim_test'), expect_val='usim-test'),
Paramtest(param_cls=p13n.K, Paramtest(param_cls=p13n.K,
val='01020304050607080910111213141516', val='01020304050607080910111213141516',
@@ -274,7 +280,102 @@ class ConfigurableParameterTest(unittest.TestCase):
expect_clean_val=3, expect_clean_val=3,
expect_val='3'), expect_val='3'),
] Paramtest(param_cls=p13n.EuiccMandatoryServiceGetIdentity,
val='mandatory',
expect_clean_val=True,
expect_val='mandatory'),
Paramtest(param_cls=p13n.EuiccMandatoryServiceGetIdentity,
val='optional',
expect_clean_val=False,
expect_val='optional'),
Paramtest(param_cls=p13n.EuiccMandatoryServiceProfileA,
val='mandatory',
expect_clean_val=True,
expect_val='mandatory'),
Paramtest(param_cls=p13n.EuiccMandatoryServiceProfileA,
val='optional',
expect_clean_val=False,
expect_val='optional'),
Paramtest(param_cls=p13n.EuiccMandatoryServiceProfileB,
val='mandatory',
expect_clean_val=True,
expect_val='mandatory'),
Paramtest(param_cls=p13n.EuiccMandatoryServiceProfileB,
val='optional',
expect_clean_val=False,
expect_val='optional'),
]
Paramtest.iff_present_default = True
sucici = {
"prot_scheme_id_list": [
{"priority": 0, "identifier": 2, "key_index": 1},
{"priority": 1, "identifier": 1, "key_index": 2},
],
"hnet_pubkey_list": [
{"hnet_pubkey_identifier": 27,
"hnet_pubkey": "0472da71976234ce833a6907425867b82e074d44ef907dfb4b3e21c1c2256ebcd15a7ded52fcbb097a4ed250e036c7b9c8c7004c4eedc4f068cd7bf8d3f900e3b4"},
{"hnet_pubkey_identifier": 30,
"hnet_pubkey": "5a8d38864820197c3394b92613b20b91633cbd897119273bf8e4a6f4eec0a650"},
],
}
param_tests.extend([
Paramtest(param_cls=p13n.SuciActive, val='SUCI-on',
expect_clean_val=True,
expect_val={'5G-SUCI-active': 'SUCI-on'}),
Paramtest(param_cls=p13n.SuciActive, val='SUCI-off',
expect_clean_val=False,
expect_val={'5G-SUCI-active': 'SUCI-off'}),
Paramtest(param_cls=p13n.SuciInUsim, val='SUCI-in-UE',
expect_clean_val=False,
expect_val={'5G-SUCI-in-USIM': 'SUCI-in-UE'}),
Paramtest(param_cls=p13n.SuciInUsim, val='SUCI-in-USIM',
expect_clean_val=True,
expect_val={'5G-SUCI-in-USIM': 'SUCI-in-USIM'}),
Paramtest(param_cls=p13n.SuciRi, val='123',
expect_clean_val='123',
expect_val={'5G-SUCI-RI': '123'}),
Paramtest(param_cls=p13n.SuciRi, val='0',
expect_clean_val='0',
expect_val={'5G-SUCI-RI': '0'}),
Paramtest(param_cls=p13n.SuciRi, val='9999',
expect_clean_val='9999',
expect_val={'5G-SUCI-RI': '9999'}),
Paramtest(param_cls=p13n.SuciCalcInfoUe,
val=json.dumps(sucici),
expect_clean_val=sucici,
expect_val={'5G-SUCI-CalcInfo-UE': json.dumps(sucici)}),
Paramtest(param_cls=p13n.SuciCalcInfoUsim,
val=json.dumps(sucici),
expect_clean_val=sucici,
expect_val={'5G-SUCI-CalcInfo-USIM': json.dumps(sucici)}),
Paramtest(param_cls=p13n.GfmSuciRi, val='123',
expect_clean_val='123',
expect_val={'GFM-5G-SUCI-RI': '123'}),
Paramtest(param_cls=p13n.GfmSuciRi, val='0',
expect_clean_val='0',
expect_val={'GFM-5G-SUCI-RI': '0'}),
Paramtest(param_cls=p13n.GfmSuciRi, val='9999',
expect_clean_val='9999',
expect_val={'GFM-5G-SUCI-RI': '9999'}),
Paramtest(param_cls=p13n.GfmSuciCalcInfoUe,
val=json.dumps(sucici),
expect_clean_val=sucici,
expect_val={'GFM-5G-SUCI-CalcInfo-UE': json.dumps(sucici)}),
])
Paramtest.iff_present_default = False
for sdkey_cls in ( for sdkey_cls in (
# thin out the number of tests, as a compromise between completeness and test runtime # thin out the number of tests, as a compromise between completeness and test runtime
@@ -367,7 +468,8 @@ class ConfigurableParameterTest(unittest.TestCase):
for t in param_tests: for t in param_tests:
test_idx += 1 test_idx += 1
logloc = f'{upp_fname} {t.param_cls.__name__}(val={valtypestr(t.val)})' testlog = []
testlog.append(f'{upp_fname} {t.param_cls.__name__}(val={valtypestr(t.val)})')
param = None param = None
try: try:
@@ -375,21 +477,32 @@ class ConfigurableParameterTest(unittest.TestCase):
param.input_value = t.val param.input_value = t.val
param.validate() param.validate()
except ValueError as e: except ValueError as e:
raise ValueError(f'{logloc}: {e}') from e raise ValueError(f'{" ".join(testlog)}: {e}') from e
clean_val = param.value clean_val = param.value
logloc = f'{logloc} clean_val={valtypestr(clean_val)}' testlog.append(f'clean_val={valtypestr(clean_val)}')
if t.expect_clean_val is not None and t.expect_clean_val != clean_val: if t.expect_clean_val is not None and t.expect_clean_val != clean_val:
raise ValueError(f'{logloc}: expected' raise ValueError(f'{" ".join(testlog)}: expected'
f' expect_clean_val={valtypestr(t.expect_clean_val)}') f' expect_clean_val={valtypestr(t.expect_clean_val)}')
# on my laptop, deepcopy is about 30% slower than decoding the DER from scratch: # on my laptop, deepcopy is about 30% slower than decoding the DER from scratch:
# pes = copy.deepcopy(orig_pes) # pes = copy.deepcopy(orig_pes)
pes = ProfileElementSequence.from_der(der) pes = ProfileElementSequence.from_der(der)
found = list((t.param_cls.get_value_from_pes(pes) or {}).values())
testlog.append(f"previous value: {found}")
if t.iff_present and not found:
testlog.append("skipping, param not in template.")
output = "\nskip: " + "\n ".join(testlog)
outputs.append(output)
print(output)
continue
try: try:
param.apply(pes) param.apply(pes)
except ValueError as e: except ValueError as e:
raise ValueError(f'{logloc} apply_val(clean_val): {e}') from e raise ValueError(f'{" ".join(testlog)} apply_val(clean_val): {e}') from e
changed_der = pes.to_der() changed_der = pes.to_der()
@@ -407,22 +520,18 @@ class ConfigurableParameterTest(unittest.TestCase):
else: else:
read_back_val_type = f'{type(read_back_val).__name__}' read_back_val_type = f'{type(read_back_val).__name__}'
logloc = (f'{logloc} read_back_val={valtypestr(read_back_val)}') testlog.append(f'read_back_val={valtypestr(read_back_val)}')
if isinstance(read_back_val, dict) and not t.param_cls.get_name() in read_back_val.keys(): if isinstance(read_back_val, dict) and not t.param_cls.get_name() in read_back_val.keys():
raise ValueError(f'{logloc}: expected to find name {t.param_cls.get_name()!r} in read_back_val') raise ValueError(f'{" ".join(testlog)}: expected to find name {t.param_cls.get_name()!r} in read_back_val')
expect_val = t.expect_val expect_val = t.expect_val
if not isinstance(expect_val, dict): if not isinstance(expect_val, dict):
expect_val = { t.param_cls.get_name(): expect_val } expect_val = { t.param_cls.get_name(): expect_val }
if read_back_val != expect_val: if read_back_val != expect_val:
raise ValueError(f'{logloc}: expected {expect_val=!r}:{type(t.expect_val).__name__}') raise ValueError(f'{" ".join(testlog)}: expected {expect_val=!r}:{type(t.expect_val).__name__}')
ok = logloc.replace(' clean_val', '\n\tclean_val' output = "\nok: " + "\n ".join(testlog)
).replace(' read_back_val', '\n\tread_back_val'
).replace('=', '=\t'
)
output = f'\nok: {ok}'
outputs.append(output) outputs.append(output)
print(output) print(output)
@@ -558,7 +667,7 @@ class TestEnumParam(unittest.TestCase):
def test_validate_by_name_exact(self): def test_validate_by_name_exact(self):
self.assertEqual(p13n.AlgorithmID.validate_val('Milenage'), 1) self.assertEqual(p13n.AlgorithmID.validate_val('Milenage'), 1)
self.assertEqual(p13n.AlgorithmID.validate_val('TUAK'), 2) self.assertEqual(p13n.AlgorithmID.validate_val('TUAK'), 2)
self.assertEqual(p13n.AlgorithmID.validate_val('usim_test'), 3) self.assertEqual(p13n.AlgorithmID.validate_val('usim-test'), 3)
def test_validate_by_int(self): def test_validate_by_int(self):
self.assertEqual(p13n.AlgorithmID.validate_val(1), 1) self.assertEqual(p13n.AlgorithmID.validate_val(1), 1)
@@ -571,7 +680,6 @@ class TestEnumParam(unittest.TestCase):
self.assertEqual(p13n.AlgorithmID.validate_val('tuak'), 2) self.assertEqual(p13n.AlgorithmID.validate_val('tuak'), 2)
def test_validate_fuzzy_hyphen_underscore(self): def test_validate_fuzzy_hyphen_underscore(self):
# 'usim-test' has a hyphen; enum member is 'usim_test' — must fuzzy-match
self.assertEqual(p13n.AlgorithmID.validate_val('usim-test'), 3) self.assertEqual(p13n.AlgorithmID.validate_val('usim-test'), 3)
def test_validate_invalid_name(self): def test_validate_invalid_name(self):
@@ -608,7 +716,7 @@ class TestEnumParam(unittest.TestCase):
def test_map_val_known(self): def test_map_val_known(self):
self.assertEqual(p13n.AlgorithmID.map_val_to_name(1), 'Milenage') self.assertEqual(p13n.AlgorithmID.map_val_to_name(1), 'Milenage')
self.assertEqual(p13n.AlgorithmID.map_val_to_name(2), 'TUAK') self.assertEqual(p13n.AlgorithmID.map_val_to_name(2), 'TUAK')
self.assertEqual(p13n.AlgorithmID.map_val_to_name(3), 'usim_test') self.assertEqual(p13n.AlgorithmID.map_val_to_name(3), 'usim-test')
def test_map_val_unknown_nonstrict(self): def test_map_val_unknown_nonstrict(self):
self.assertIsNone(p13n.AlgorithmID.map_val_to_name(99)) self.assertIsNone(p13n.AlgorithmID.map_val_to_name(99))
@@ -622,7 +730,9 @@ class TestEnumParam(unittest.TestCase):
def test_name_normalize(self): def test_name_normalize(self):
self.assertEqual(p13n.AlgorithmID.name_normalize('Milenage'), 'Milenage') self.assertEqual(p13n.AlgorithmID.name_normalize('Milenage'), 'Milenage')
self.assertEqual(p13n.AlgorithmID.name_normalize('milenage'), 'Milenage') self.assertEqual(p13n.AlgorithmID.name_normalize('milenage'), 'Milenage')
self.assertEqual(p13n.AlgorithmID.name_normalize('usim-test'), 'usim_test') self.assertEqual(p13n.AlgorithmID.name_normalize('usimtest'), 'usim-test')
self.assertEqual(p13n.AlgorithmID.name_normalize('usim_test'), 'usim-test')
self.assertEqual(p13n.AlgorithmID.name_normalize('USIM Test'), 'usim-test')
# --- clean_name_str --- # --- clean_name_str ---
File diff suppressed because it is too large Load Diff