dexter has uploaded this change for review.

View Change

saip-tool: add features to add and remove applications profile elements

The PE-Application object is used to provision JAVA-card applications
into an eUICC during profile installation. Let's extend the SAIP-tool
so that we are able to add and remove applications.

Change-Id: I41db96f2f0ccc29c1725a92215ce6b17d87b76ce
---
A contrib/saip-tool-add-app.sh
M contrib/saip-tool.py
M pySim/esim/saip/__init__.py
3 files changed, 330 insertions(+), 12 deletions(-)

git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/60/39860/1
diff --git a/contrib/saip-tool-add-app.sh b/contrib/saip-tool-add-app.sh
new file mode 100755
index 0000000..2f0cd0e
--- /dev/null
+++ b/contrib/saip-tool-add-app.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# This is an example script to illustrate how to add JAVA card applets to an existing eUICC profile package.
+
+PYSIMPATH=../
+INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE.der
+OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
+APPPATH=./HelloSTK_09122024.cap
+
+# Download example applet (see also https://gitea.osmocom.org/sim-card/hello-stk):
+if ! [ -f $APPPATH ]; then
+ wget https://osmocom.org/attachments/download/8931/HelloSTK_09122024.cap
+fi
+
+# Step #1: Create the application PE and load the ijc contents from the .cap file:
+PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH add-app \
+ --output-file $OUTPATH --applet-file $APPPATH --aid 'D07002CA44'
+
+# Step #2: Create the application instance inside the application PE created in step #1:
+PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH add-app-inst --output-file $OUTPATH \
+ --aid 'D07002CA44' \
+ --class-aid 'D07002CA44900101' \
+ --inst-aid 'D07002CA44900101' \
+ --app-privileges '00' \
+ --app-spec-pars '00' \
+ --uicc-toolkit-app-spec-pars '01001505000000000000000000000000'
+
+# Display the contents of the resulting application PE:
+PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH list-apps
+
+# For an explaination of --uicc-toolkit-app-spec-pars, see:
+# eUICC Profile Package: Interoperable Format Technical Specification spec, section 17.2.3.2)
diff --git a/contrib/saip-tool.py b/contrib/saip-tool.py
index b7282bd..134357a 100755
--- a/contrib/saip-tool.py
+++ b/contrib/saip-tool.py
@@ -19,14 +19,13 @@
import sys
import argparse
import logging
-import zipfile
from pathlib import Path as PlPath
from typing import List
from osmocom.utils import h2b, b2h, swap_nibbles
+from osmocom.construct import GreedyBytes, StripHeaderAdapter

from pySim.esim.saip import *
from pySim.esim.saip.validation import CheckBasicStructure
-from pySim import javacard
from pySim.pprint import HexBytesPrettyPrinter

pp = HexBytesPrettyPrinter(indent=4,width=500)
@@ -59,10 +58,45 @@

parser_info = subparsers.add_parser('info', help='Display information about the profile')

+parser_lapp = subparsers.add_parser('list-apps', help='List applications and their related instances')
+
parser_eapp = subparsers.add_parser('extract-apps', help='Extract applications as loadblock file')
parser_eapp.add_argument('--output-dir', default='.', help='Output directory (where to store files)')
parser_eapp.add_argument('--format', default='cap', choices=['ijc', 'cap'], help='Data format of output files')

+parser_aapp = subparsers.add_parser('add-app', help='Add application to PE-Sequence')
+parser_aapp.add_argument('--output-file', required=True, help='Output file name')
+parser_aapp.add_argument('--applet-file', required=True, help='Applet file name')
+parser_aapp.add_argument('--aid', required=True, help='Load package AID')
+parser_aapp.add_argument('--sd-aid', default=None, help='Security Domain AID')
+parser_aapp.add_argument('--non-volatile-code-limit', default=None, type=int, help='Non volatile code limit (C6)')
+parser_aapp.add_argument('--volatile-data-limit', default=None, type=int, help='Volatile data limit (C7)')
+parser_aapp.add_argument('--non-volatile-data-limit', default=None, type=int, help='Non volatile data limit (C8)')
+parser_aapp.add_argument('--hash-value', default=None, help='Hash value')
+
+parser_rapp = subparsers.add_parser('remove-app', help='Remove application from PE-Sequence')
+parser_rapp.add_argument('--output-file', required=True, help='Output file name')
+parser_rapp.add_argument('--aid', required=True, help='Load package AID')
+
+parser_aappi = subparsers.add_parser('add-app-inst', help='Add application instance to Application PE')
+parser_aappi.add_argument('--output-file', required=True, help='Output file name')
+parser_aappi.add_argument('--aid', required=True, help='Load package AID')
+parser_aappi.add_argument('--class-aid', required=True, help='Class AID')
+parser_aappi.add_argument('--inst-aid', required=True, help='Instance AID (must match Load package AID)')
+parser_aappi.add_argument('--app-privileges', default='000000', help='Application privileges')
+parser_aappi.add_argument('--volatile-memory-quota', default=None, type=int, help='Volatile memory quota (C7)')
+parser_aappi.add_argument('--non-volatile-memory-quota', default=None, type=int, help='Non volatile memory quota (C8)')
+parser_aappi.add_argument('--app-spec-pars', default='00', help='Application specific parameters (C9)')
+parser_aappi.add_argument('--uicc-toolkit-app-spec-pars', help='UICC toolkit application specific parameters field')
+parser_aappi.add_argument('--uicc-access-app-spec-pars', help='UICC Access application specific parameters field')
+parser_aappi.add_argument('--uicc-adm-access-app-spec-pars', help='UICC Administrative access application specific parameters field')
+parser_aappi.add_argument('--process-data', default=[], action='append', help='Process personalization APDUs')
+
+parser_rappi = subparsers.add_parser('remove-app-inst', help='Remove application instance from Application PE')
+parser_rappi.add_argument('--output-file', required=True, help='Output file name')
+parser_rappi.add_argument('--aid', required=True, help='Load package AID')
+parser_rappi.add_argument('--inst-aid', required=True, help='Instance AID')
+
parser_info = subparsers.add_parser('tree', help='Display the filesystem tree')

def write_pes(pes: ProfileElementSequence, output_file:str):
@@ -211,20 +245,195 @@
for tar in tar_list:
print("\tTAR: %s" % b2h(tar))

+def do_list_apps(pes:ProfileElementSequence, opts):
+ def show_member(dictionary:dict, member:str, indent:str="\t", mandatory:bool = False, limit:bool = False):
+ if dictionary is None:
+ return
+ value = dictionary.get(member, None)
+ if value is None and mandatory == True:
+ print("%s%s: (missing!)" % (indent, member))
+ elif value is None:
+ return
+ if limit and len(value) > 40:
+ print("%s%s: '%s...%s' (%u bytes)" % (indent, member, b2h(value[:20]), b2h(value[-20:]), len(value)))
+ else:
+ print("%s%s: '%s' (%u bytes)" % (indent, member, b2h(value), len(value)))
+
+ apps = pes.pe_by_type.get('application', [])
+ if len(apps) == 0:
+ print("No Application PE present!")
+ return;
+
+ app_index = 0
+ for app_pe in apps:
+ print("Application #%u:" % app_index)
+ app_index += 1
+ pe_index = pes.get_index_by_pe(app_pe)
+ print("\tIndex in PE sequence: %u" % pe_index)
+ print("\tloadBlock:")
+ load_block = app_pe.decoded['loadBlock']
+ show_member(load_block, 'loadPackageAID', "\t\t", True)
+ show_member(load_block, 'securityDomainAID', "\t\t")
+ show_member(load_block, 'nonVolatileCodeLimitC6', "\t\t")
+ show_member(load_block, 'volatileDataLimitC7', "\t\t")
+ show_member(load_block, 'nonVolatileDataLimitC8', "\t\t")
+ show_member(load_block, 'hashValue', "\t\t")
+ show_member(load_block, 'loadBlockObject', "\t\t", True, True)
+ inst_index = 0
+ for inst in app_pe.decoded.get('instanceList', []):
+ print("\tinstanceList[%u]:" % inst_index)
+ inst_index += 1
+ show_member(inst, 'applicationLoadPackageAID', "\t\t", True)
+ if inst.get('applicationLoadPackageAID', None) != load_block.get('loadPackageAID', None):
+ print("\t\t(applicationLoadPackageAID should be the same as loadPackageAID!)")
+ show_member(inst, 'classAID', "\t\t", True)
+ show_member(inst, 'instanceAID', "\t\t", True)
+ show_member(inst, 'extraditeSecurityDomainAID', "\t\t")
+ show_member(inst, 'applicationPrivileges', "\t\t", True)
+ show_member(inst, 'lifeCycleState', "\t\t", True)
+ show_member(inst, 'applicationSpecificParametersC9', "\t\t", True)
+ sys_specific_pars = inst.get('systemSpecificParameters', None)
+ if sys_specific_pars:
+ print("\t\tsystemSpecificParameters:")
+ show_member(sys_specific_pars, 'volatileMemoryQuotaC7', "\t\t\t")
+ show_member(sys_specific_pars, 'nonVolatileMemoryQuotaC8', "\t\t\t")
+ show_member(sys_specific_pars, 'globalServiceParameters', "\t\t\t")
+ show_member(sys_specific_pars, 'implicitSelectionParameter', "\t\t\t")
+ show_member(sys_specific_pars, 'volatileReservedMemory', "\t\t\t")
+ show_member(sys_specific_pars, 'nonVolatileReservedMemory', "\t\t\t")
+ show_member(sys_specific_pars, 'ts102226SIMFileAccessToolkitParameter', "\t\t\t")
+ additional_cl_pars = inst.get('ts102226AdditionalContactlessParameters', None)
+ if additional_cl_pars:
+ print("\t\t\tts102226AdditionalContactlessParameters:")
+ show_member(additional_cl_pars, 'protocolParameterData', "\t\t\t\t")
+ show_member(sys_specific_pars, 'userInteractionContactlessParameters', "\t\t\t")
+ show_member(sys_specific_pars, 'cumulativeGrantedVolatileMemory', "\t\t\t")
+ show_member(sys_specific_pars, 'cumulativeGrantedNonVolatileMemory', "\t\t\t")
+ app_pars = inst.get('applicationParameters', None)
+ if app_pars:
+ print("\t\tapplicationParameters:")
+ show_member(app_pars, 'uiccToolkitApplicationSpecificParametersField', "\t\t\t")
+ show_member(app_pars, 'uiccAccessApplicationSpecificParametersField', "\t\t\t")
+ show_member(app_pars, 'uiccAdministrativeAccessApplicationSpecificParametersField', "\t\t\t")
+ ctrl_ref_tp = inst.get('controlReferenceTemplate', None)
+ if ctrl_ref_tp:
+ print("\t\tcontrolReferenceTemplate:")
+ show_member(ctrl_ref_tp, 'applicationProviderIdentifier', "\t\t\t", True)
+ process_data = inst.get('processData', None)
+ if process_data:
+ print("\t\tprocessData:")
+ for proc in process_data:
+ print("\t\t\t" + b2h(proc))
+
def do_extract_apps(pes:ProfileElementSequence, opts):
apps = pes.pe_by_type.get('application', [])
for app_pe in apps:
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
-
fname = os.path.join(opts.output_dir, '%s-%s.%s' % (pes.iccid, package_aid, opts.format))
- load_block_obj = app_pe.decoded['loadBlock']['loadBlockObject']
print("Writing Load Package AID: %s to file %s" % (package_aid, fname))
- if opts.format == 'ijc':
- with open(fname, 'wb') as f:
- f.write(load_block_obj)
- else:
- with io.BytesIO(load_block_obj) as f, zipfile.ZipFile(fname, 'w') as z:
- javacard.ijc_to_cap(f, z, package_aid)
+ app_pe.to_file(fname)
+
+def do_add_app(pes:ProfileElementSequence, opts):
+ print("Applying applet file: '%s'..." % opts.applet_file)
+ app_pe = ProfileElementApplication.from_file(opts.applet_file,
+ opts.aid,
+ opts.sd_aid,
+ opts.non_volatile_code_limit,
+ opts.volatile_data_limit,
+ opts.non_volatile_data_limit,
+ opts.hash_value)
+
+ security_domain = pes.pe_by_type.get('securityDomain', [])
+ pes.insert_after_pe(security_domain[0], app_pe)
+ print("application PE inserted into PE Sequence after securityDomain PE AID: %s" %
+ b2h(security_domain[0].decoded['instance']['instanceAID']))
+
+ write_pes(pes, opts.output_file)
+
+def do_remove_app(pes:ProfileElementSequence, opts):
+ apps = pes.pe_by_type.get('application', [])
+ for app_pe in apps:
+ package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
+ if opts.aid == package_aid:
+ identification = app_pe.identification
+ opts_remove_pe = argparse.Namespace()
+ opts_remove_pe.identification = [app_pe.identification]
+ opts_remove_pe.output_file = opts.output_file
+ print("Found Load Package AID: %s, removing related PE (id=%u) from Sequence..." %
+ (package_aid, identification))
+ do_remove_pe(pes, opts_remove_pe)
+ return
+
+ print("Load Package AID: %s not found in PE Sequence" % opts.aid)
+
+def do_add_app_inst(pes:ProfileElementSequence, opts):
+ apps = pes.pe_by_type.get('application', [])
+ for app_pe in apps:
+ package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
+ if opts.aid == package_aid:
+ print("Found Load Package AID: %s" % package_aid)
+
+ # Mandatory
+ inst = {'applicationLoadPackageAID': h2b(opts.aid),
+ 'classAID': h2b(opts.class_aid),
+ 'instanceAID': h2b(opts.inst_aid),
+ 'applicationPrivileges': h2b(opts.app_privileges),
+ 'applicationSpecificParametersC9': h2b(opts.app_spec_pars)}
+
+ # Optional
+ if opts.uicc_toolkit_app_spec_pars or opts.uicc_access_app_spec_pars or opts.uicc_adm_access_app_spec_pars:
+ inst['applicationParameters'] = {}
+ if opts.uicc_toolkit_app_spec_pars:
+ inst['applicationParameters']['uiccToolkitApplicationSpecificParametersField'] = \
+ h2b(opts.uicc_toolkit_app_spec_pars)
+ if opts.uicc_access_app_spec_pars:
+ inst['applicationParameters']['uiccAccessApplicationSpecificParametersField'] = \
+ h2b(opts.uicc_access_app_spec_pars)
+ if opts.uicc_adm_access_app_spec_pars:
+ inst['applicationParameters']['uiccAdministrativeAccessApplicationSpecificParametersField'] = \
+ h2b(opts.uicc_adm_access_app_spec_pars)
+ if opts.volatile_memory_quota is not None or opts.non_volatile_memory_quota is not None:
+ inst['systemSpecificParameters'] = {}
+ Construct_data_limit = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
+ if opts.volatile_memory_quota is not None:
+ inst['systemSpecificParameters']['volatileMemoryQuotaC7'] = \
+ Construct_data_limit.build(opts.volatile_memory_quota)
+ if opts.non_volatile_memory_quota is not None:
+ inst['systemSpecificParameters']['nonVolatileMemoryQuotaC8'] = \
+ Construct_data_limit.build(opts.non_volatile_memory_quota)
+ if len(opts.process_data) > 0:
+ inst['processData'] = []
+ for proc in opts.process_data:
+ inst['processData'].append(h2b(proc))
+
+ print("Adding new instance AID: %s to Application PE..." % opts.inst_aid)
+ if 'instanceList' not in app_pe.decoded.keys():
+ app_pe.decoded['instanceList'] = []
+ app_pe.decoded['instanceList'].append(inst)
+
+ write_pes(pes, opts.output_file)
+ return
+
+ print("Load Package AID: %s not found in PE Sequence" % opts.aid)
+
+def do_remove_app_inst(pes:ProfileElementSequence, opts):
+ apps = pes.pe_by_type.get('application', [])
+ for app_pe in apps:
+ package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
+ if opts.aid == package_aid:
+ print("Found Load Package AID: %s" % package_aid)
+ inst_index = 0
+ inst_list = app_pe.decoded.get('instanceList', [])
+ for inst_index in range(len(inst_list)):
+ if b2h(inst_list[inst_index].get('instanceAID', None)) == opts.inst_aid:
+ print("Found related Instance AID: %s at instanceList index %u" % (opts.inst_aid, inst_index))
+ print("Removing instance from Application PE...")
+ inst_list.pop(inst_index)
+ write_pes(pes, opts.output_file)
+ return
+ print("Instance AID: %s not found in Application PE" % opts.aid)
+
+ print("Load Package AID: %s not found in PE Sequence" % opts.aid)

def do_tree(pes:ProfileElementSequence, opts):
pes.mf.print_tree()
@@ -254,7 +463,17 @@
do_remove_naa(pes, opts)
elif opts.command == 'info':
do_info(pes, opts)
+ elif opts.command == 'list-apps':
+ do_list_apps(pes, opts)
elif opts.command == 'extract-apps':
do_extract_apps(pes, opts)
+ elif opts.command == 'add-app':
+ do_add_app(pes, opts)
+ elif opts.command == 'remove-app':
+ do_remove_app(pes, opts)
+ elif opts.command == 'add-app-inst':
+ do_add_app_inst(pes, opts)
+ elif opts.command == 'remove-app-inst':
+ do_remove_app_inst(pes, opts)
elif opts.command == 'tree':
do_tree(pes, opts)
diff --git a/pySim/esim/saip/__init__.py b/pySim/esim/saip/__init__.py
index 64ea7cb..752df74 100644
--- a/pySim/esim/saip/__init__.py
+++ b/pySim/esim/saip/__init__.py
@@ -22,9 +22,11 @@
from typing import Tuple, List, Optional, Dict, Union
from collections import OrderedDict
import asn1tools
+import zipfile
+from pySim import javacard
from osmocom.utils import b2h, h2b, Hexstr
from osmocom.tlv import BER_TLV_IE, bertlv_parse_tag, bertlv_parse_len
-from osmocom.construct import build_construct, parse_construct, GreedyInteger
+from osmocom.construct import build_construct, parse_construct, GreedyInteger, GreedyBytes, StripHeaderAdapter

from pySim import ts_102_222
from pySim.utils import dec_imsi
@@ -37,6 +39,7 @@
from pySim.esim.saip import oid
from pySim.global_platform import KeyType, KeyUsageQualifier
from pySim.global_platform.uicc import UiccSdInstallParams
+from pySim.javacard import CapFile

asn1 = compile_asn1_subdir('saip')

@@ -506,7 +509,7 @@
# TODO: cdmaParameter
'securityDomain': ProfileElementSD,
'rfm': ProfileElementRFM,
- # TODO: application
+ 'application': ProfileElementApplication,
# TODO: nonStandard
'end': ProfileElementEnd,
'mf': ProfileElementMF,
@@ -1087,6 +1090,71 @@
'uiccToolkitApplicationSpecificParametersField': h2b('01000001000000020112036C756500'),
}

+class ProfileElementApplication(ProfileElement):
+ """Class representing an application ProfileElement."""
+ type = 'application'
+
+ def __init__(self, decoded: Optional[dict] = None, **kwargs):
+ super().__init__(decoded, **kwargs)
+
+ @classmethod
+ def from_file(cls,
+ filename:str,
+ aid:Hexstr,
+ sd_aid:Hexstr = None,
+ non_volatile_code_limit:int = None,
+ volatile_data_limit:int = None,
+ non_volatile_data_limit:int = None,
+ hash_value:Hexstr = None) -> 'ProfileElementApplication':
+ """Fill contents of application ProfileElement from a .cap file."""
+
+ inst = cls()
+ Construct_data_limit = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
+
+ if filename.lower().endswith('.cap'):
+ cap = CapFile(filename)
+ load_block_object = cap.get_loadfile()
+ elif filename.lower().endswith('.ijc'):
+ fd = open(filename, 'rb')
+ load_block_object = fd.read()
+ else:
+ raise ValueError('Invalid file type, file must either .cap or .ijc')
+
+ # Mandatory
+ inst.decoded['loadBlock'] = {
+ 'loadPackageAID': h2b(aid),
+ 'loadBlockObject': load_block_object
+ }
+
+ # Optional
+ if sd_aid:
+ inst.decoded['loadBlock']['securityDomainAID'] = h2b(sd_aid)
+ if non_volatile_code_limit:
+ inst.decoded['loadBlock']['nonVolatileCodeLimitC6'] = Construct_data_limit.build(non_volatile_code_limit)
+ if volatile_data_limit:
+ inst.decoded['loadBlock']['volatileDataLimitC7'] = Construct_data_limit.build(volatile_data_limit)
+ if non_volatile_data_limit:
+ inst.decoded['loadBlock']['nonVolatileDataLimitC8'] = Construct_data_limit.build(non_volatile_data_limit)
+ if hash_value:
+ inst.decoded['loadBlock']['hashValue'] = h2b(hash_value)
+
+ return inst
+
+ def to_file(self, filename:str):
+ """Write loadBlockObject contents of application ProfileElement to a .cap or .ijc file."""
+
+ load_package_aid = b2h(self.decoded['loadBlock']['loadPackageAID'])
+ load_block_object = self.decoded['loadBlock']['loadBlockObject']
+
+ if filename.lower().endswith('.cap'):
+ with io.BytesIO(load_block_object) as f, zipfile.ZipFile(filename, 'w') as z:
+ javacard.ijc_to_cap(f, z, load_package_aid)
+ elif filename.lower().endswith('.ijc'):
+ with open(filename, 'wb') as f:
+ f.write(load_block_object)
+ else:
+ raise ValueError('Invalid file type, file must either .cap or .ijc')
+
class ProfileElementRFM(ProfileElement):
type = 'rfm'


To view, visit change 39860. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-MessageType: newchange
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I41db96f2f0ccc29c1725a92215ce6b17d87b76ce
Gerrit-Change-Number: 39860
Gerrit-PatchSet: 1
Gerrit-Owner: dexter <pmaier@sysmocom.de>