mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-08-15 07:31:44 +03:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de151a24e1 | |||
| 06641b743c | |||
| 3ae1d2a614 | |||
| ba61973131 | |||
| 15939cb67e | |||
| eb6d0af925 | |||
| c892a48829 | |||
| a1b313e6ec | |||
| c4fc946ac8 | |||
| b0a4216f69 | |||
| 26cd602d0d | |||
| 7593a40e64 | |||
| 49b61f3c14 | |||
| e0cee1f4b4 | |||
| f557e14984 | |||
| 09c41b05fd | |||
| a9b1fe0ae4 | |||
| 45e2be5ab6 | |||
| c682421c1b | |||
| 4b3b04219d | |||
| 1f43f267ee | |||
| 75c639fb6c | |||
| 733394caa5 | |||
| d2ab5c2afb |
+1
-1
@@ -117,7 +117,7 @@ class Tracer:
|
||||
try:
|
||||
apdu = self.source.read()
|
||||
apdu_counter = apdu_counter + 1
|
||||
except (StopIteration, KeyboardInterrupt):
|
||||
except StopIteration:
|
||||
print("%i APDUs parsed, stop iteration." % apdu_counter)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -411,20 +411,30 @@ class JsonHttpApiClient():
|
||||
# SGP.22, section 6.5.1)
|
||||
if response.status_code != self.api_func.expected_http_status:
|
||||
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)
|
||||
|
||||
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
|
||||
raise HttpHeaderError(response)
|
||||
|
||||
# Decode response and return the result back to the caller
|
||||
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
|
||||
# (the presence of the header field is checked by the decode_client method)
|
||||
if 'header' in output:
|
||||
if output['header']['functionExecutionStatus']['status'] not in ['Executed-Success','Executed-WithWarning']:
|
||||
raise ApiError(output['header']['functionExecutionStatus'])
|
||||
return output
|
||||
|
||||
return None
|
||||
|
||||
class JsonHttpApiServer():
|
||||
|
||||
@@ -34,7 +34,7 @@ from pySim import ts_102_222
|
||||
from pySim.utils import dec_imsi
|
||||
from pySim.ts_102_221 import FileDescriptor
|
||||
from pySim.filesystem import CardADF, Path
|
||||
from pySim.ts_31_102 import ADF_USIM
|
||||
from pySim.ts_31_102 import ADF_USIM, EF_UST, EF_SUCI_Calc_Info
|
||||
from pySim.ts_31_103 import ADF_ISIM
|
||||
from pySim.esim import compile_asn1_subdir
|
||||
from pySim.esim.saip import templates
|
||||
@@ -1517,8 +1517,11 @@ class ProfileElementHeader(ProfileElement):
|
||||
def mandatory_service_add(self, service_name):
|
||||
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):
|
||||
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]
|
||||
else:
|
||||
raise ValueError("service not in eUICC-Mandatory-services list, cannot remove")
|
||||
@@ -1726,12 +1729,61 @@ class ProfileElementSequence:
|
||||
if 'BT' in ftype_list:
|
||||
svc_set.add('ber-tlv')
|
||||
# FIXME:dfLinked files (scan all files, check for non-empty Fcp.linkPath presence of DFs)
|
||||
# TODO: 5G related bits (derive from EF.UST or file presence?)
|
||||
|
||||
# 5G:
|
||||
# - When SUCI is:
|
||||
# - enabled (EF.UST 124 = true)
|
||||
# AND
|
||||
# - calculated in the USIM (EF.UST 125 = true),
|
||||
# then eUICC-Mandatory-services needs 'get-identity'.
|
||||
# - 'get-identity' implies that the eUICC must support ONE OF profile-A OR profile-B.
|
||||
# (One might assume from this that, when SUCI-CalcInfo for USIM in DF.SAIP contains both key types, then no
|
||||
# profile-A or B services need to be requested explicitly. However, the correct logic is:)
|
||||
# - Iff the SUCI-CalcInfo for USIM (DF.SAIP) contains a key of profile-A ("identifier": 1),
|
||||
# then eUICC-Mandatory-services needs 'profile-a-x25519'.
|
||||
# - Same: profile-B ("identifier": 2) needs 'profile-b-p256'.
|
||||
# - (When SUCI is calculated in the UE, then the eUICC does not need to provide any of these services.)
|
||||
suci_in_usim_enabled = False
|
||||
try:
|
||||
f_ust = self.get_pe_for_type("usim").files["ef-ust"]
|
||||
ust = EF_UST().decode_bin(f_ust.body)
|
||||
suci_in_usim_enabled = ust[124]['activated'] and ust[125]['activated']
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
if suci_in_usim_enabled:
|
||||
svc_set.add('get-identity')
|
||||
# now check for profile-a and profile-b presence
|
||||
suci_calcinfo_has_profile_a = False
|
||||
suci_calcinfo_has_profile_b = False
|
||||
try:
|
||||
f_sucici = self.get_pe_for_type("df-saip").files["ef-suci-calc-info-usim"]
|
||||
sucici = EF_SUCI_Calc_Info().decode_bin(f_sucici.body) or {}
|
||||
for prot_scheme in sucici['prot_scheme_id_list']:
|
||||
if not isinstance(prot_scheme, dict):
|
||||
continue
|
||||
ps_id = prot_scheme["identifier"]
|
||||
if ps_id == 1:
|
||||
suci_calcinfo_has_profile_a = True
|
||||
elif ps_id == 2:
|
||||
suci_calcinfo_has_profile_b = True
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
if suci_calcinfo_has_profile_a:
|
||||
# The profile has a profile-A key, so require that
|
||||
svc_set.add('profile-a-x25519')
|
||||
if suci_calcinfo_has_profile_b:
|
||||
# The profile has a profile-B key, so require that
|
||||
svc_set.add('profile-b-p256')
|
||||
|
||||
hdr_pe = self.get_pe_for_type('header')
|
||||
# patch in the 'manual' services from the existing list:
|
||||
old_svc_set = set()
|
||||
for old_svc in hdr_pe.decoded['eUICC-Mandatory-services'].keys():
|
||||
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}
|
||||
|
||||
def rebuild_mandatory_gfstelist(self):
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
|
||||
import copy
|
||||
import pprint
|
||||
import logging
|
||||
import traceback
|
||||
import inspect
|
||||
from typing import Generator, Union
|
||||
from pySim.esim.saip.personalization import ConfigurableParameter
|
||||
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 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
|
||||
ParamList = list[Union[type[ConfigurableParameter], ConfigurableParameter]]
|
||||
|
||||
@@ -121,8 +128,12 @@ class BatchPersonalization:
|
||||
value = p.param_cls.validate_val(input_value)
|
||||
p.param_cls.apply_val(pes, value)
|
||||
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
|
||||
|
||||
pes.rebuild_mandatory_services()
|
||||
|
||||
yield pes
|
||||
|
||||
|
||||
@@ -322,12 +333,15 @@ class BatchAudit(list):
|
||||
|
||||
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."""
|
||||
columns = set()
|
||||
for audit in self:
|
||||
columns.update(audit.keys())
|
||||
|
||||
if column_blacklist:
|
||||
columns.difference_update(set(column_blacklist))
|
||||
|
||||
columns = tuple(sorted(columns, key=sort_key))
|
||||
|
||||
if headers:
|
||||
|
||||
+512
-104
@@ -19,19 +19,26 @@ import abc
|
||||
import enum
|
||||
import io
|
||||
import re
|
||||
import json
|
||||
from typing import List, Tuple, Generator, Optional
|
||||
|
||||
from construct.core import StreamError
|
||||
from osmocom.tlv import camel_to_snake
|
||||
from osmocom.utils import hexstr
|
||||
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.esim.saip import param_source
|
||||
from pySim.esim.saip import ProfileElement, ProfileElementSD, ProfileElementSequence
|
||||
from pySim.esim.saip import ProfileElementHeader
|
||||
from pySim.esim.saip import SecurityDomainKey, SecurityDomainKeyComponent
|
||||
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:
|
||||
return hexstr(s.rstrip(c))
|
||||
|
||||
@@ -290,7 +297,9 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
||||
May be overridden by 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
|
||||
def is_super_of(cls, other_class):
|
||||
@@ -420,69 +429,71 @@ class BinaryParam(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
|
||||
integer codes. apply_val() and get_values_from_pes() are not implemented here and this must
|
||||
be inherited from another mixin."""
|
||||
|
||||
class Values(enum.IntEnum):
|
||||
pass # subclasses override this
|
||||
Subclasses define an own value_map, and implement their own apply_val() and get_values_from_pes().
|
||||
"""
|
||||
value_map = {
|
||||
# For example:
|
||||
#'Meaningful label for value 23': 0x23,
|
||||
# Where 0x23 is a valid value to use for apply_val(), of any valid type.
|
||||
}
|
||||
_value_map_reverse = None
|
||||
|
||||
@classmethod
|
||||
def validate_val(cls, val) -> int:
|
||||
if isinstance(val, int):
|
||||
try:
|
||||
return int(cls.Values(val))
|
||||
except ValueError:
|
||||
pass
|
||||
elif isinstance(val, str):
|
||||
member = cls.map_name_to_val(val, strict=False)
|
||||
if member is not None:
|
||||
return member
|
||||
def validate_val(cls, val):
|
||||
orig_val = val
|
||||
enum_val = None
|
||||
if isinstance(val, str):
|
||||
enum_name = val
|
||||
enum_val = cls.map_name_to_val(enum_name)
|
||||
|
||||
valid = ', '.join(m.name for m in cls.Values)
|
||||
raise ValueError(f"{cls.get_name()}: invalid argument: {val!r}. Valid arguments are: {valid}")
|
||||
# if the str is not one of the known value_map.keys(), is it maybe one of value_map.keys()?
|
||||
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
|
||||
def map_name_to_val(cls, name: str, strict=True) -> int:
|
||||
"""Return the integer value for a given enum member name. Performs an exact match first,
|
||||
then falls back to fuzzy matching (case-insensitive, punctuation-insensitive)."""
|
||||
try:
|
||||
return int(cls.Values[name])
|
||||
except KeyError:
|
||||
pass
|
||||
def map_name_to_val(cls, name:str, strict=True):
|
||||
val = cls.value_map.get(name)
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
clean = cls.clean_name_str(name)
|
||||
for member in cls.Values:
|
||||
if cls.clean_name_str(member.name) == clean:
|
||||
return int(member)
|
||||
clean_name = cls.clean_name_str(name)
|
||||
for k, v in cls.value_map.items():
|
||||
if clean_name == cls.clean_name_str(k):
|
||||
return v
|
||||
|
||||
if strict:
|
||||
valid = ', '.join(m.name for m in cls.Values)
|
||||
raise ValueError(f"{cls.get_name()}: {name!r} is not a known value. Known values are: {valid}")
|
||||
raise ValueError(f"Problem in {cls.get_name()}: {name!r} is not a known value."
|
||||
f" Known values are: {cls.value_map.keys()!r}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def map_val_to_name(cls, val, strict=False) -> str:
|
||||
"""Return the enum member name for a given integer value."""
|
||||
try:
|
||||
return cls.Values(val).name
|
||||
except ValueError:
|
||||
if strict:
|
||||
raise ValueError(f"{cls.get_name()}: {val!r} ({type(val).__name__}) is not a known value.")
|
||||
return None
|
||||
if cls._value_map_reverse is None:
|
||||
cls._value_map_reverse = dict((v, k) for k, v in cls.value_map.items())
|
||||
|
||||
name = cls._value_map_reverse.get(val)
|
||||
if name:
|
||||
return name
|
||||
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
|
||||
def name_normalize(cls, name: str) -> str:
|
||||
"""Map a (possibly fuzzy) name to its canonical enum member name."""
|
||||
return cls.Values(cls.map_name_to_val(name)).name
|
||||
def name_normalize(cls, name:str) -> str:
|
||||
return cls.map_val_to_name(cls.map_name_to_val(name))
|
||||
|
||||
@classmethod
|
||||
def clean_name_str(cls, val: str) -> str:
|
||||
"""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()
|
||||
def clean_name_str(cls, val):
|
||||
return re.sub('[^0-9A-Za-z-_]', '', val).lower()
|
||||
|
||||
|
||||
class Iccid(DecimalParam):
|
||||
@@ -635,13 +646,14 @@ class SmspTpScAddr(ConfigurableParameter):
|
||||
# - 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
|
||||
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.
|
||||
#
|
||||
#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)
|
||||
# re-generate the pe.decoded member from the File instance
|
||||
pe.file2pe(f_smsp)
|
||||
|
||||
@classmethod
|
||||
@@ -663,69 +675,57 @@ class SmspTpScAddr(ConfigurableParameter):
|
||||
|
||||
|
||||
class MncLen(EnumParam):
|
||||
"""MNC length. 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'.
|
||||
"""
|
||||
"""MNC length. Must be either 2 or 3. Sets only the MNC length field in EF-AD (Administrative Data)."""
|
||||
name = 'MNC-LEN'
|
||||
example_input = '2'
|
||||
value_map = { '2': 2, '3': 3 }
|
||||
default_source = param_source.ConstantSource
|
||||
|
||||
class Values(enum.IntEnum):
|
||||
MNC2 = 2
|
||||
MNC3 = 3
|
||||
example_input = '2'
|
||||
|
||||
@classmethod
|
||||
def validate_val(cls, val):
|
||||
if isinstance(val, str) and val.isdigit():
|
||||
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):
|
||||
def apply_val(cls, pes: ProfileElementSequence, val):
|
||||
"""val must be an int: either 2 or 3"""
|
||||
for pe in pes.get_pes_for_type('usim'):
|
||||
f_ad = cls._get_f_ad(pe)
|
||||
if f_ad is None:
|
||||
if not hasattr(pe, 'files'):
|
||||
continue
|
||||
f_ad = pe.files.get('ef-ad')
|
||||
if not f_ad:
|
||||
continue
|
||||
# decode existing values
|
||||
ef_ad_dec = cls._decode_f_ad(f_ad)
|
||||
if ef_ad_dec is None:
|
||||
if not f_ad.body:
|
||||
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
|
||||
# change mnc_len
|
||||
ef_ad_dec['mnc_len'] = val
|
||||
# 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)
|
||||
|
||||
@classmethod
|
||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
||||
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:
|
||||
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
|
||||
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):
|
||||
@@ -1099,17 +1099,17 @@ class AlgorithmID(EnumParam, AlgoConfig):
|
||||
"""use validate_val() from EnumParam, and apply_val() from AlgoConfig.
|
||||
In get_values_from_pes(), return enum value names, not raw values."""
|
||||
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'
|
||||
example_input = "Milenage"
|
||||
default_source = param_source.ConstantSource
|
||||
|
||||
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
|
||||
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
|
||||
# EnumParam.validate_val() returns the int values from value_map
|
||||
|
||||
@classmethod
|
||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
||||
@@ -1156,7 +1156,7 @@ class MilenageRotationConstants(BinaryParam, AlgoConfig):
|
||||
|
||||
class MilenageXoringConstants(BinaryParam, AlgoConfig):
|
||||
"""XOR-ing constants c1,c2,c3,c4,c5 of Milenage, 128bit each. See 3GPP TS 35.206 Sections 2.3 + 5.3.
|
||||
Provided as octet-string concatenation of all 5 constants. The default value by 3GPP is the concetenation
|
||||
Provided as octet-string concatenation of all 5 constants. The default value by 3GPP is the concatenation
|
||||
of::
|
||||
|
||||
00000000000000000000000000000000
|
||||
@@ -1184,3 +1184,411 @@ class TuakNumberOfKeccak(IntegerParam, AlgoConfig):
|
||||
max_val = 255
|
||||
example_input = '1'
|
||||
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 then 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 = bool(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'
|
||||
|
||||
+2
-10
@@ -285,14 +285,6 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
{"hnet_pubkey_identifier": 11, "hnet_pubkey":
|
||||
h2b("d1bc365f4997d17ce4374e72181431cbfeba9e1b98d7618f79d48561b144672a")}]} ),
|
||||
]
|
||||
_test_decode = [
|
||||
( 'A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
|
||||
{"prot_scheme_id_list": [],
|
||||
"hnet_pubkey_list": []} ),
|
||||
( 'A000A100FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
|
||||
{"prot_scheme_id_list": [],
|
||||
"hnet_pubkey_list": []} ),
|
||||
]
|
||||
# 3GPP TS 31.102 Section 4.4.11.8
|
||||
class ProtSchemeIdList(BER_TLV_IE, tag=0xa0):
|
||||
# FIXME: 3GPP TS 24.501 Protection Scheme Identifier
|
||||
@@ -335,7 +327,7 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
"""conversion method to generate list of {hnet_pubkey_identifier, hnet_pubkey} dicts
|
||||
from flat [{hnet_pubkey_identifier: }, {net_pubkey: }, ...] list"""
|
||||
out = []
|
||||
while len(l):
|
||||
while l:
|
||||
a = l.pop(0)
|
||||
b = l.pop(0)
|
||||
z = {**a, **b}
|
||||
@@ -397,7 +389,7 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
# remaining data holds Home Network Public Key Data Object
|
||||
hpkl = EF_SUCI_Calc_Info.HnetPubkeyList()
|
||||
hpkl.from_tlv(in_bytes[pos:])
|
||||
hnet_pubkey_list = self._compact_pubkey_list(hpkl.to_dict()['hnet_pubkey_list'] or [])
|
||||
hnet_pubkey_list = self._compact_pubkey_list(hpkl.to_dict()['hnet_pubkey_list'])
|
||||
|
||||
return {
|
||||
'prot_scheme_id_list': prot_scheme_id_list,
|
||||
|
||||
Binary file not shown.
@@ -21,6 +21,7 @@ import enum
|
||||
import io
|
||||
import sys
|
||||
import unittest
|
||||
import json
|
||||
from importlib import resources
|
||||
from osmocom.utils import hexstr
|
||||
from pySim.esim.saip import ProfileElementSequence
|
||||
@@ -53,18 +54,21 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
def test_parameters(self):
|
||||
|
||||
upp_fnames = (
|
||||
'TS48v5_SAIP2.1A_NoBERTLV.der',
|
||||
'TS48v5_SAIP2.3_BERTLV_SUCI.der',
|
||||
'SAIP2.1_gfmsuci.der',
|
||||
'TS48v5_SAIP2.1B_NoBERTLV.der',
|
||||
'TS48v5_SAIP2.3_NoBERTLV.der',
|
||||
)
|
||||
|
||||
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.val = val
|
||||
self.expect_clean_val = expect_clean_val
|
||||
self.expect_val = expect_val
|
||||
if iff_present is None:
|
||||
iff_present = Paramtest.iff_present_default
|
||||
self.iff_present = iff_present
|
||||
|
||||
param_tests = [
|
||||
Paramtest(param_cls=p13n.Imsi, val='123456',
|
||||
@@ -150,7 +154,7 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
Paramtest(param_cls=p13n.AlgorithmID,
|
||||
val='usim-test',
|
||||
expect_clean_val=3,
|
||||
expect_val='usim_test'),
|
||||
expect_val='usim-test'),
|
||||
|
||||
Paramtest(param_cls=p13n.AlgorithmID,
|
||||
val=1,
|
||||
@@ -163,7 +167,7 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
Paramtest(param_cls=p13n.AlgorithmID,
|
||||
val=3,
|
||||
expect_clean_val=3,
|
||||
expect_val='usim_test'),
|
||||
expect_val='usim-test'),
|
||||
|
||||
Paramtest(param_cls=p13n.K,
|
||||
val='01020304050607080910111213141516',
|
||||
@@ -276,7 +280,102 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
expect_clean_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 (
|
||||
# thin out the number of tests, as a compromise between completeness and test runtime
|
||||
@@ -369,7 +468,8 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
|
||||
for t in param_tests:
|
||||
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
|
||||
try:
|
||||
@@ -377,21 +477,32 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
param.input_value = t.val
|
||||
param.validate()
|
||||
except ValueError as e:
|
||||
raise ValueError(f'{logloc}: {e}') from e
|
||||
raise ValueError(f'{" ".join(testlog)}: {e}') from e
|
||||
|
||||
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:
|
||||
raise ValueError(f'{logloc}: expected'
|
||||
raise ValueError(f'{" ".join(testlog)}: expected'
|
||||
f' expect_clean_val={valtypestr(t.expect_clean_val)}')
|
||||
|
||||
# on my laptop, deepcopy is about 30% slower than decoding the DER from scratch:
|
||||
# pes = copy.deepcopy(orig_pes)
|
||||
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:
|
||||
param.apply(pes)
|
||||
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()
|
||||
|
||||
@@ -409,22 +520,18 @@ class ConfigurableParameterTest(unittest.TestCase):
|
||||
else:
|
||||
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():
|
||||
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
|
||||
if not isinstance(expect_val, dict):
|
||||
expect_val = { t.param_cls.get_name(): 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'
|
||||
).replace(' read_back_val', '\n\tread_back_val'
|
||||
).replace('=', '=\t'
|
||||
)
|
||||
output = f'\nok: {ok}'
|
||||
output = "\nok: " + "\n ".join(testlog)
|
||||
outputs.append(output)
|
||||
print(output)
|
||||
|
||||
@@ -560,7 +667,7 @@ class TestEnumParam(unittest.TestCase):
|
||||
def test_validate_by_name_exact(self):
|
||||
self.assertEqual(p13n.AlgorithmID.validate_val('Milenage'), 1)
|
||||
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):
|
||||
self.assertEqual(p13n.AlgorithmID.validate_val(1), 1)
|
||||
@@ -573,7 +680,6 @@ class TestEnumParam(unittest.TestCase):
|
||||
self.assertEqual(p13n.AlgorithmID.validate_val('tuak'), 2)
|
||||
|
||||
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)
|
||||
|
||||
def test_validate_invalid_name(self):
|
||||
@@ -610,7 +716,7 @@ class TestEnumParam(unittest.TestCase):
|
||||
def test_map_val_known(self):
|
||||
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(3), 'usim_test')
|
||||
self.assertEqual(p13n.AlgorithmID.map_val_to_name(3), 'usim-test')
|
||||
|
||||
def test_map_val_unknown_nonstrict(self):
|
||||
self.assertIsNone(p13n.AlgorithmID.map_val_to_name(99))
|
||||
@@ -624,13 +730,13 @@ class TestEnumParam(unittest.TestCase):
|
||||
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('usim-test'), 'usim_test')
|
||||
self.assertEqual(p13n.AlgorithmID.name_normalize('usim-test'), 'usim-test')
|
||||
|
||||
# --- clean_name_str ---
|
||||
|
||||
def test_clean_name_str(self):
|
||||
self.assertEqual(p13n.AlgorithmID.clean_name_str('usim-test'), 'usimtest')
|
||||
self.assertEqual(p13n.AlgorithmID.clean_name_str('usim_test'), 'usimtest')
|
||||
self.assertEqual(p13n.AlgorithmID.clean_name_str('usim-test'), 'usim-test')
|
||||
self.assertEqual(p13n.AlgorithmID.clean_name_str('usim_test'), 'usim_test')
|
||||
self.assertEqual(p13n.AlgorithmID.clean_name_str('Milenage'), 'milenage')
|
||||
self.assertEqual(p13n.AlgorithmID.clean_name_str('foo bar!'), 'foobar')
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user