laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/37137?usp=email )
Change subject: pySim.tlv: Correctly parse COMPREHENSION-TLV without comprehension bit
......................................................................
pySim.tlv: Correctly parse COMPREHENSION-TLV without comprehension bit
The uppermost bit of COMPREHENSION-TLV tags indicates whether the
recipient is required to "comprehend" that IE or not. So every IE
actually has two tag values: one with and one without that bit set.
As all our existing TLV definitions of COMPR_TLV_IE have that bit set,
let's assume this is the default, but use the same definition also for
the situation where that bit is not set.
Change-Id: I58d04ec13be0c12d9fb8cb3d5a0480d0defb6c95
---
M pySim/tlv.py
1 file changed, 33 insertions(+), 1 deletion(-)
Approvals:
Jenkins Builder: Verified
laforge: Looks good to me, approved
osmith: Looks good to me, but someone else must approve
diff --git a/pySim/tlv.py b/pySim/tlv.py
index 8bf43c6..5835d33 100644
--- a/pySim/tlv.py
+++ b/pySim/tlv.py
@@ -224,12 +224,16 @@
val = self.to_bytes(context=context)
return self._encode_tag() + self._encode_len(val) + val
+ def is_tag_compatible(self, rawtag) -> bool:
+ """Is the given rawtag compatible with this class?"""
+ return rawtag == self._compute_tag()
+
def from_tlv(self, do: bytes, context: dict = {}):
if len(do) == 0:
return {}, b''
(rawtag, remainder) = self.__class__._parse_tag_raw(do)
if rawtag:
- if rawtag != self._compute_tag():
+ if not self.is_tag_compatible(rawtag):
raise ValueError("%s: Encountered tag %s doesn't match our supported tag %s" %
(self, rawtag, self.tag))
(length, remainder) = self.__class__._parse_len(remainder)
@@ -283,6 +287,15 @@
def _parse_len(cls, do: bytes) -> Tuple[int, bytes]:
return bertlv_parse_len(do)
+ def is_tag_compatible(self, rawtag: int) -> bool:
+ """Override is_tag_compatible as we need to mask out the
+ comprehension bit when doing compares."""
+ ctag = self._compute_tag()
+ if ctag > 0xff:
+ return ctag & 0x7fff == rawtag & 0x7fff
+ else:
+ return ctag & 0x7f == rawtag & 0x7f
+
def _encode_tag(self) -> bytes:
return comprehensiontlv_encode_tag(self._compute_tag())
@@ -367,6 +380,8 @@
tag, _r = first._parse_tag_raw(remainder)
if tag is None:
break
+ if issubclass(first, COMPR_TLV_IE):
+ tag = tag | 0x80 # HACK: always assume comprehension
if tag in self.members_by_tag:
cls = self.members_by_tag[tag]
# create an instance and parse accordingly
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/37137?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I58d04ec13be0c12d9fb8cb3d5a0480d0defb6c95
Gerrit-Change-Number: 37137
Gerrit-PatchSet: 1
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
Gerrit-MessageType: merged
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/37010?usp=email )
Change subject: esim.saip: Add ProfileElementSequence.remove_naas_of_type
......................................................................
esim.saip: Add ProfileElementSequence.remove_naas_of_type
This method allows the caller to remove all NAAs of a certain type,
for example to remove all CSIM instances from a given profile.
Change-Id: I64438bf0be58bad7a561c3744b7e9b1338a7857c
---
M pySim/esim/saip/__init__.py
M pySim/esim/saip/oid.py
2 files changed, 99 insertions(+), 0 deletions(-)
Approvals:
Jenkins Builder: Verified
laforge: Looks good to me, approved
osmith: Looks good to me, but someone else must approve
diff --git a/pySim/esim/saip/__init__.py b/pySim/esim/saip/__init__.py
index 9fa568c..3742c96 100644
--- a/pySim/esim/saip/__init__.py
+++ b/pySim/esim/saip/__init__.py
@@ -27,12 +27,58 @@
from pySim.construct import build_construct
from pySim.esim import compile_asn1_subdir
from pySim.esim.saip import templates
+from pySim.esim.saip import oid
from pySim.tlv import BER_TLV_IE
from pySim.global_platform import KeyType, KeyUsageQualifier
from pySim.global_platform.uicc import UiccSdInstallParams
asn1 = compile_asn1_subdir('saip')
+class Naa:
+ """A class defining a Network Access Application (NAA)."""
+ name = None
+ # AID prefix, as used for ADF and EF.DIR
+ aid = None
+ # the ProfileElement types used specifically in this NAA
+ pe_types = []
+ # we only use the base DN of each OID; there may be subsequent versions underneath it
+ templates = []
+ mandatory_services = []
+
+ @classmethod
+ def adf_name(cls):
+ return 'adf-' + cls.mandatory_services[0]
+
+class NaaCsim(Naa):
+ name = "csim"
+ aid = h2b("")
+ mandatory_services = ["csim"]
+ pe_types = ["csim", "opt-csim", "cdmaParameter"]
+ templates = [oid.ADF_CSIM_by_default, oid.ADF_CSIM_not_by_default]
+
+class NaaUsim(Naa):
+ name = "usim"
+ aid = h2b("")
+ mandatory_services = ["usim"]
+ pe_types = ["usim", "opt-usim"]
+ templates = [oid.ADF_USIM_by_default, oid.ADF_USIM_not_by_default,
+ oid.DF_PHONEBOOK_ADF_USIM, oid.DF_GSM_ACCESS_ADF_USIM,
+ oid.DF_EAP, oid.DF_5GS, oid.DF_SAIP, oid.DF_SNPN,
+ oid.DF_5GProSe]
+
+class NaaIsim(Naa):
+ name = "isim"
+ aid = h2b("")
+ mandatory_services = ["isim"]
+ pe_types = ["isim", "opt-isim"]
+ templates = [oid.ADF_ISIM_by_default, oid.ADF_ISIM_not_by_default]
+
+NAAs = {
+ NaaCsim.name: NaaCsim,
+ NaaUsim.name: NaaUsim,
+ NaaIsim.name: NaaIsim,
+}
+
class File:
"""Internal representation of a file in a profile filesystem.
@@ -513,6 +559,43 @@
self._process_pelist()
self.renumber_identification()
+ def remove_naas_of_type(self, naa: Naa) -> None:
+ """Remove all instances of NAAs of given type. This can be used, for example,
+ to remove all CSIM NAAs from a profile. Will not just remove the PEs, but also
+ any records in 'eUICC-Mandatory-services' or 'eUICC-Mandatory-GFSTEList'."""
+ hdr = self.pe_by_type['header'][0]
+ # remove any associated mandatory services
+ for service in naa.mandatory_services:
+ if service in hdr.decoded['eUICC-Mandatory-services']:
+ del hdr.decoded['eUICC-Mandatory-services'][service]
+ # remove any associaed mandatory filesystem templates
+ for template in naa.templates:
+ if template in hdr.decoded['eUICC-Mandatory-GFSTEList']:
+ hdr.decoded['eUICC-Mandatory-GFSTEList'] = [x for x in hdr.decoded['eUICC-Mandatory-GFSTEList'] if not template.prefix_match(x)]
+ # determine the ADF names (AIDs) of all NAA ADFs
+ naa_adf_names = []
+ if naa.pe_types[0] in self.pe_by_type:
+ for pe in self.pe_by_type[naa.pe_types[0]]:
+ adf_name = naa.adf_name()
+ adf = File(adf_name, pe.decoded[adf_name])
+ naa_adf_names.append(adf.fileDescriptor['dfName'])
+ # remove PEs of each NAA instance
+ if naa.name in self.pes_by_naa:
+ for inst in self.pes_by_naa[naa.name]:
+ # delete all the PEs of the NAA
+ self.pe_list = [pe for pe in self.pe_list if pe not in inst]
+ self._process_pelist()
+ # remove any RFM PEs for the just-removed ADFs
+ if 'rfm' in self.pe_by_type:
+ to_delete_pes = []
+ for rfm_pe in self.pe_by_type['rfm']:
+ if 'adfRFMAccess' in rfm_pe.decoded:
+ if rfm_pe.decoded['adfRFMAccess']['adfAID'] in naa_adf_names:
+ to_delete_pes.append(rfm_pe)
+ self.pe_list = [pe for pe in self.pe_list if pe not in to_delete_pes]
+ self._process_pelist()
+ # TODO: remove any records related to the ADFs from EF.DIR
+
def __repr__(self) -> str:
return "PESequence(%s)" % ', '.join([str(x) for x in self.pe_list])
diff --git a/pySim/esim/saip/oid.py b/pySim/esim/saip/oid.py
index 238e49f..d33b786 100644
--- a/pySim/esim/saip/oid.py
+++ b/pySim/esim/saip/oid.py
@@ -38,6 +38,10 @@
def __repr__(self) -> str:
return 'OID(%s)' % (str(self))
+ def prefix_match(self, oid_str):
+ """determine if oid_str is equal or below our OID."""
+ return oid_str.startswith(str(self))
+
class eOID(OID):
"""OID helper for TCA eUICC prefix"""
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/37010?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I64438bf0be58bad7a561c3744b7e9b1338a7857c
Gerrit-Change-Number: 37010
Gerrit-PatchSet: 4
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
Gerrit-MessageType: merged
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/37145?usp=email )
Change subject: pySim.tlv.COMPR_TLV_IE: Patch comprehension bit if derived class misses it
......................................................................
pySim.tlv.COMPR_TLV_IE: Patch comprehension bit if derived class misses it
Our current implementation assumes that all COMPR_TLV_IE are created
with a raw tag value that has the comprehension bit set. Check for this
during the class __new__ method and print a warning if we have to fix it up
Change-Id: I299cd65f32dffda9040d18c17a374e8dc9ebe7da
---
M pySim/tlv.py
1 file changed, 28 insertions(+), 1 deletion(-)
Approvals:
laforge: Looks good to me, approved
Jenkins Builder: Verified
osmith: Looks good to me, but someone else must approve
diff --git a/pySim/tlv.py b/pySim/tlv.py
index 5835d33..3d7a420 100644
--- a/pySim/tlv.py
+++ b/pySim/tlv.py
@@ -268,7 +268,21 @@
return bertlv_encode_len(len(val))
-class COMPR_TLV_IE(TLV_IE):
+class ComprTlvMeta(TlvMeta):
+ def __new__(mcs, name, bases, namespace, **kwargs):
+ x = super().__new__(mcs, name, bases, namespace)
+ if x.tag:
+ # we currently assume that the tag values always have the comprehension bit set;
+ # let's fix it up if a derived class has forgotten about that
+ if x.tag > 0xff and x.tag & 0x8000 == 0:
+ print("Fixing up COMPR_TLV_IE class %s: tag=0x%x has no comprehension bit" % (name, x.tag))
+ x.tag = x.tag | 0x8000
+ elif x.tag & 0x80 == 0:
+ print("Fixing up COMPR_TLV_IE class %s: tag=0x%x has no comprehension bit" % (name, x.tag))
+ x.tag = x.tag | 0x80
+ return x
+
+class COMPR_TLV_IE(TLV_IE, metaclass=ComprTlvMeta):
"""TLV_IE formated as COMPREHENSION-TLV as described in ETSI TS 101 220."""
def __init__(self, **kwargs):
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/37145?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I299cd65f32dffda9040d18c17a374e8dc9ebe7da
Gerrit-Change-Number: 37145
Gerrit-PatchSet: 3
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
Gerrit-MessageType: merged
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/37011?usp=email )
Change subject: add contrib/saip-tool.py
......................................................................
add contrib/saip-tool.py
This is a tool to work with eSIM profiles in SAIP format. It allows
to dump the contents, run constraint checkers as well as splitting
of the PE-Sequence into the individual PEs.
Change-Id: I396bcd594e0628dfc26bd90233317a77e2f91b20
---
M contrib/jenkins.sh
A contrib/saip-tool.py
A pySim/pprint.py
3 files changed, 249 insertions(+), 1 deletion(-)
Approvals:
Jenkins Builder: Verified
osmith: Looks good to me, but someone else must approve
laforge: Looks good to me, approved
diff --git a/contrib/jenkins.sh b/contrib/jenkins.sh
index 96c4d2b..7d3defb 100755
--- a/contrib/jenkins.sh
+++ b/contrib/jenkins.sh
@@ -58,7 +58,8 @@
--enable W0301 \
pySim tests/*.py *.py \
contrib/es2p_client.py \
- contrib/es9p_client.py
+ contrib/es9p_client.py \
+ contrib/saip-tool.py
;;
"docs")
rm -rf docs/_build
diff --git a/contrib/saip-tool.py b/contrib/saip-tool.py
new file mode 100755
index 0000000..9bbaddf
--- /dev/null
+++ b/contrib/saip-tool.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+
+# (C) 2024 by Harald Welte <laforge(a)osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import argparse
+import logging
+from pathlib import Path
+from typing import List
+
+from pySim.esim.saip import *
+from pySim.esim.saip.validation import CheckBasicStructure
+from pySim.utils import h2b, b2h, swap_nibbles
+from pySim.pprint import HexBytesPrettyPrinter
+
+pp = HexBytesPrettyPrinter(indent=4,width=500)
+
+logging.basicConfig(level=logging.DEBUG)
+
+parser = argparse.ArgumentParser(description="""
+Utility program to work with eSIM SAIP (SimAlliance Interoperable Profile) files.""")
+parser.add_argument('INPUT_UPP', help='Unprotected Profile Package Input file')
+subparsers = parser.add_subparsers(dest='command', help="The command to perform", required=True)
+
+parser_split = subparsers.add_parser('split', help='Split PE-Sequence into individual PEs')
+parser_split.add_argument('--output-prefix', default='.', help='Prefix path/filename for output files')
+
+parser_dump = subparsers.add_parser('dump', help='Dump information on PE-Sequence')
+parser_dump.add_argument('mode', choices=['all_pe', 'all_pe_by_type', 'all_pe_by_naa'])
+parser_dump.add_argument('--dump-decoded', action='store_true', help='Dump decoded PEs')
+
+parser_check = subparsers.add_parser('check', help='Run constraint checkers on PE-Sequence')
+
+parser_rpe = subparsers.add_parser('remove-pe', help='Remove specified PEs from PE-Sequence')
+parser_rpe.add_argument('--output-file', required=True, help='Output file name')
+parser_rpe.add_argument('--identification', type=int, action='append', help='Remove PEs matching specified identification')
+
+parser_rn = subparsers.add_parser('remove-naa', help='Remove speciifed NAAs from PE-Sequence')
+parser_rn.add_argument('--output-file', required=True, help='Output file name')
+parser_rn.add_argument('--naa-type', required=True, choices=NAAs.keys(), help='Network Access Application type to remove')
+# TODO: add an --naa-index or the like, so only one given instance can be removed
+
+
+def do_split(pes: ProfileElementSequence, opts):
+ i = 0
+ for pe in pes.pe_list:
+ basename = Path(opts.INPUT_UPP).stem
+ if not pe.identification:
+ fname = '%s-%02u-%s.der' % (basename, i, pe.type)
+ else:
+ fname = '%s-%02u-%05u-%s.der' % (basename, i, pe.identification, pe.type)
+ print("writing single PE to file '%s'" % fname)
+ with open(os.path.join(opts.output_prefix, fname), 'wb') as outf:
+ outf.write(pe.to_der())
+ i += 1
+
+def do_dump(pes: ProfileElementSequence, opts):
+ def print_all_pe(pes: ProfileElementSequence, dump_decoded:bool = False):
+ # iterate over each pe in the pes (using its __iter__ method)
+ for pe in pes:
+ print("="*70 + " " + pe.type)
+ if dump_decoded:
+ pp.pprint(pe.decoded)
+
+ def print_all_pe_by_type(pes: ProfileElementSequence, dump_decoded:bool = False):
+ # sort by PE type and show all PE within that type
+ for pe_type in pes.pe_by_type.keys():
+ print("="*70 + " " + pe_type)
+ for pe in pes.pe_by_type[pe_type]:
+ pp.pprint(pe)
+ if dump_decoded:
+ pp.pprint(pe.decoded)
+
+ def print_all_pe_by_naa(pes: ProfileElementSequence, dump_decoded:bool = False):
+ for naa in pes.pes_by_naa:
+ i = 0
+ for naa_instance in pes.pes_by_naa[naa]:
+ print("="*70 + " " + naa + str(i))
+ i += 1
+ for pe in naa_instance:
+ pp.pprint(pe.type)
+ if dump_decoded:
+ for d in pe.decoded:
+ print(" %s" % d)
+
+ if opts.mode == 'all_pe':
+ print_all_pe(pes, opts.dump_decoded)
+ elif opts.mode == 'all_pe_by_type':
+ print_all_pe_by_type(pes, opts.dump_decoded)
+ elif opts.mode == 'all_pe_by_naa':
+ print_all_pe_by_naa(pes, opts.dump_decoded)
+
+def do_check(pes: ProfileElementSequence, opts):
+ print("Checking PE-Sequence structure...")
+ checker = CheckBasicStructure()
+ checker.check(pes)
+ print("All good!")
+
+def do_remove_pe(pes: ProfileElementSequence, opts):
+ new_pe_list = []
+ for pe in pes.pe_list:
+ identification = pe.identification
+ if identification:
+ if identification in opts.identification:
+ print("Removing PE %s (id=%u) from Sequence..." % (pe, identification))
+ continue
+ new_pe_list.append(pe)
+
+ pes.pe_list = new_pe_list
+ pes._process_pelist()
+ print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
+ with open(opts.output_file, 'wb') as f:
+ f.write(pes.to_der())
+
+def do_remove_naa(pes: ProfileElementSequence, opts):
+ if not opts.naa_type in NAAs:
+ raise ValueError('unsupported NAA type %s' % opts.naa_type)
+ naa = NAAs[opts.naa_type]
+ print("Removing NAAs of type '%s' from Sequence..." % opts.naa_type)
+ pes.remove_naas_of_type(naa)
+ print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
+ with open(opts.output_file, 'wb') as f:
+ f.write(pes.to_der())
+
+
+if __name__ == '__main__':
+ opts = parser.parse_args()
+
+ with open(opts.INPUT_UPP, 'rb') as f:
+ pes = ProfileElementSequence.from_der(f.read())
+
+ print("Read %u PEs from file '%s'" % (len(pes.pe_list), opts.INPUT_UPP))
+
+ if opts.command == 'split':
+ do_split(pes, opts)
+ elif opts.command == 'dump':
+ do_dump(pes, opts)
+ elif opts.command == 'check':
+ do_check(pes, opts)
+ elif opts.command == 'remove-pe':
+ do_remove_pe(pes, opts)
+ elif opts.command == 'remove-naa':
+ do_remove_naa(pes, opts)
diff --git a/pySim/pprint.py b/pySim/pprint.py
new file mode 100644
index 0000000..965c01b
--- /dev/null
+++ b/pySim/pprint.py
@@ -0,0 +1,77 @@
+import pprint
+from pprint import PrettyPrinter
+from functools import singledispatch, wraps
+from typing import get_type_hints
+
+from pySim.utils import b2h
+
+def common_container_checks(f):
+ type_ = get_type_hints(f)['object']
+ base_impl = type_.__repr__
+ empty_repr = repr(type_()) # {}, [], ()
+ too_deep_repr = f'{empty_repr[0]}...{empty_repr[-1]}' # {...}, [...], (...)
+ @wraps(f)
+ def wrapper(object, context, maxlevels, level):
+ if type(object).__repr__ is not base_impl: # subclassed repr
+ return repr(object)
+ if not object: # empty, short-circuit
+ return empty_repr
+ if maxlevels and level >= maxlevels: # exceeding the max depth
+ return too_deep_repr
+ oid = id(object)
+ if oid in context: # self-reference
+ return pprint._recursion(object)
+ context[oid] = 1
+ result = f(object, context, maxlevels, level)
+ del context[oid]
+ return result
+ return wrapper
+
+@singledispatch
+def saferepr(object, context, maxlevels, level):
+ return repr(object)
+
+(a)saferepr.register
+def _handle_bytes(object: bytes, *args):
+ if len(object) <= 40:
+ return '"%s"' % b2h(object)
+ else:
+ return '"%s...%s"' % (b2h(object[:20]), b2h(object[-20:]))
+
+(a)saferepr.register
+@common_container_checks
+def _handle_dict(object: dict, context, maxlevels, level):
+ level += 1
+ contents = [
+ f'{saferepr(k, context, maxlevels, level)}: '
+ f'{saferepr(v, context, maxlevels, level)}'
+ for k, v in sorted(object.items(), key=pprint._safe_tuple)
+ ]
+ return f'{{{", ".join(contents)}}}'
+
+(a)saferepr.register
+@common_container_checks
+def _handle_list(object: list, context, maxlevels, level):
+ level += 1
+ contents = [
+ f'{saferepr(v, context, maxlevels, level)}'
+ for v in object
+ ]
+ return f'[{", ".join(contents)}]'
+
+(a)saferepr.register
+@common_container_checks
+def _handle_tuple(object: tuple, context, maxlevels, level):
+ level += 1
+ if len(object) == 1:
+ return f'({saferepr(object[0], context, maxlevels, level)},)'
+ contents = [
+ f'{saferepr(v, context, maxlevels, level)}'
+ for v in object
+ ]
+ return f'({", ".join(contents)})'
+
+class HexBytesPrettyPrinter(PrettyPrinter):
+ def format(self, *args):
+ # it doesn't matter what the boolean values are here
+ return saferepr(*args), True, False
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/37011?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I396bcd594e0628dfc26bd90233317a77e2f91b20
Gerrit-Change-Number: 37011
Gerrit-PatchSet: 8
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
Gerrit-MessageType: merged