From 712946eddb9eedce30b44276d7bd75f40c9a69b9 Mon Sep 17 00:00:00 2001 From: Philipp Maier Date: Thu, 19 Dec 2024 17:59:51 +0100 Subject: [PATCH] javacard: add parser for JAVA-card CAP file format To install JAVA-card applets we need to be able to extract the executeable loadfile and the AIDs of the applet and the loadfile. This patch adds the parser and related unittests. Related: OS#6679 Change-Id: I581483ccb9d8a254fcecc995fec3c811c5cf38eb --- pySim/javacard.py | 123 +++++++++++++++++++++++++++++- tests/unittests/test_javacard.cap | Bin 0 -> 6173 bytes tests/unittests/test_javacard.py | 18 +++++ 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/test_javacard.cap create mode 100755 tests/unittests/test_javacard.py diff --git a/pySim/javacard.py b/pySim/javacard.py index 07b0f399..6005ab4b 100644 --- a/pySim/javacard.py +++ b/pySim/javacard.py @@ -1,9 +1,30 @@ # JavaCard related utilities - +# +# (C) 2024 by Sysmocom s.f.m.c. GmbH +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# import zipfile import struct import sys import io +from osmocom.utils import b2h, Hexstr +from construct import Struct, Array, this, Int32ub, Int16ub, Int8ub +from osmocom.construct import * +from osmocom.tlv import * +from construct import Optional as COptional def ijc_to_cap(in_file: io.IOBase, out_zip: zipfile.ZipFile, p : str = "foo"): """Convert an ICJ (Interoperable Java Card) file [back] to a CAP file. @@ -19,3 +40,103 @@ def ijc_to_cap(in_file: io.IOBase, out_zip: zipfile.ZipFile, p : str = "foo"): out_zip.writestr(p+"/javacard/"+TAGS[tag-1]+".cap", b[0:3+size]) b = b[3+size:] +class CapFile(): + + # Java Card Platform Virtual Machine Specification, v3.2, section 6.4 + __header_component_compact = Struct('tag'/Int8ub, + 'size'/Int16ub, + 'magic'/Int32ub, + 'minor_version'/Int8ub, + 'major_version'/Int8ub, + 'flags'/Int8ub, + 'package'/Struct('minor_version'/Int8ub, + 'major_version'/Int8ub, + 'AID'/LV), + 'package_name'/COptional(LV)) #since CAP format 2.2 + + # Java Card Platform Virtual Machine Specification, v3.2, section 6.6 + __applet_component_compact = Struct('tag'/Int8ub, + 'size'/Int16ub, + 'count'/Int8ub, + 'applets'/Array(this.count, Struct('AID'/LV, + 'install_method_offset'/Int16ub)), + ) + + def __init__(self, filename:str): + + # In this dictionary we will keep all nested .cap file components by their file names (without .cap suffix) + # See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2.1 + self.__component = {} + + # Extract the nested .cap components from the .cap file + # See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2.1 + cap = zipfile.ZipFile(filename) + cap_namelist = cap.namelist() + for i, filename in enumerate(cap_namelist): + if filename.lower().endswith('.capx') and not filename.lower().endswith('.capx'): + #TODO: At the moment we only support the compact .cap format, add support for the extended .cap format. + raise ValueError("incompatible .cap file, extended .cap format not supported!") + + if filename.lower().endswith('.cap'): + key = filename.split('/')[-1].removesuffix('.cap') + self.__component[key] = cap.read(filename) + + # Make sure that all mandatory components are present + # See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2 + required_components = {'Header' : 'COMPONENT_Header', + 'Directory' : 'COMPONENT_Directory', + 'Import' : 'COMPONENT_Import', + 'ConstantPool' : 'COMPONENT_ConstantPool', + 'Class' : 'COMPONENT_Class', + 'Method' : 'COMPONENT_Method', + 'StaticField' : 'COMPONENT_StaticField', + 'RefLocation' : 'COMPONENT_ReferenceLocation', + 'Descriptor' : 'COMPONENT_Descriptor'} + for component in required_components: + if component not in self.__component.keys(): + raise ValueError("invalid cap file, %s missing!" % required_components[component]) + + def get_loadfile(self) -> bytes: + """Get the executeable loadfile as hexstring""" + # Concatenate all cap file components in the specified order + # see also: Java Card Platform Virtual Machine Specification, v3.2, section 6.3 + loadfile = self.__component['Header'] + loadfile += self.__component['Directory'] + loadfile += self.__component['Import'] + if 'Applet' in self.__component.keys(): + loadfile += self.__component['Applet'] + loadfile += self.__component['Class'] + loadfile += self.__component['Method'] + loadfile += self.__component['StaticField'] + if 'Export' in self.__component.keys(): + loadfile += self.__component['Export'] + loadfile += self.__component['ConstantPool'] + loadfile += self.__component['RefLocation'] + if 'Descriptor' in self.__component.keys(): + loadfile += self.__component['Descriptor'] + return loadfile + + def get_loadfile_aid(self) -> Hexstr: + """Get the loadfile AID as hexstring""" + header = self.__header_component_compact.parse(self.__component['Header']) + magic = header['magic'] or 0 + if magic != 0xDECAFFED: + raise ValueError("invalid cap file, COMPONENT_Header lacks magic number (0x%08X!=0xDECAFFED)!" % magic) + #TODO: check cap version and make sure we are compatible with it + return header['package']['AID'] + + def get_applet_aid(self, index:int = 0) -> Hexstr: + """Get the applet AID as hexstring""" + #To get the module AID, we must look into COMPONENT_Applet. Unfortunately, even though this component should + #be present in any .cap file, it is defined as an optional component. + if 'Applet' not in self.__component.keys(): + raise ValueError("can't get the AID, this cap file lacks the optional COMPONENT_Applet component!") + + applet = self.__applet_component_compact.parse(self.__component['Applet']) + + if index > applet['count']: + raise ValueError("can't get the AID for applet with index=%u, this .cap file only has %u applets!" % + (index, applet['count'])) + + return applet['applets'][index]['AID'] + diff --git a/tests/unittests/test_javacard.cap b/tests/unittests/test_javacard.cap new file mode 100644 index 0000000000000000000000000000000000000000..f96ea240c889ee777de911cb7ff16c6b6f1d0170 GIT binary patch literal 6173 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 Q>!C6D`#Hj9O*X6j7y0!zl>h($ literal 0 HcmV?d00001 diff --git a/tests/unittests/test_javacard.py b/tests/unittests/test_javacard.py new file mode 100755 index 00000000..0363623c --- /dev/null +++ b/tests/unittests/test_javacard.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +import os +import unittest +from pySim.javacard import * + +class TestJavacard(unittest.TestCase): + + def test_CapFile(self): + loadfile="01000fdecaffed010204000105d07002ca4402001f000f001f000c002800420018006d00320017000000a900040002002203010004002803020107a0000000620101060210a0000000090003ffffffff8910710002000107a000000062000103000c0108d07002ca44900101002006001843800301ff0007020000002f00398002008101010881000007006d000911188c00048d00012c18197b0002037b00029210240303038b000388007a02318f00053d8c00062e1b8b00077a0120188b000860037a7a02228d00092d1d10076b101a8b000a321fae006b06188c000b7a06118d000c2c1903077b000d037b000d928b000e198b000f3b7a08003200040002000203001857656c636f6d6520746f20546f6f7243616d70203230313203000a48656c6c6f2c2053544b0000000005004200100200000006810900050000020381090b0680030001000000060000010380030103800303068108000381080c0600005306810a000500000003810a1303810a1609001700021e2d0011050306040908040507090a0a06070404040b00a901000100000300030005800281018100ff080000000028ff08000002002800020000008003ff820001002f001d0000000000090020003f000d000000000701002f0042000800000000080100390046001800000000ff020053002f0018000000000010002200240028002a002fffff002f002f003100330022002f00370028003b002201300568109001b008b44323430110012005681080056810a00633b44104b431066800a10231" + cap = CapFile(os.path.dirname(os.path.abspath(__file__)) + "/test_javacard.cap") + self.assertTrue(b2h(cap.get_loadfile()) == loadfile) + self.assertTrue(cap.get_loadfile_aid() == "d07002ca44") + self.assertTrue(cap.get_applet_aid() == "d07002ca44900101") + + +if __name__ == "__main__": + unittest.main()