В статье «Асимметричная криптография в Java Card» был приведен пример по установке защищенного канала в два этапа:
генерация общего секрета по алгоритму ECDH;
использование общего секрета в качестве ключа AES.
В этой статье речь пойдет о написании библиотек (апплеты, не наследующие от javacard.framework.Applet), их загрузке и линковке с апплетами на карте. По касательной затронем VSCode, библиотеку взаимодействия с картой FunGP и нюансы инструмента сборки .cap-файлов ant-javacard.
Исходники: libutils, SimpleApplet и FunGP.
Библиотека libutils.jar
Не мудрствуя лукаво решил написать простую библиотеку с тремя понятными функциями:
«исключающее ИЛИ» над входными данными:
public void xor(byte[] buff, short off, short len) { short bound = (short)(off + len); for (short i = off; i < bound; ++i) { buff[i] ^= (short)0xFF; } }}
«пузырьковая сортировка» входных данных:
public void bubble_sort(byte[] buff, short off, short len) { byte temp = 0; short curr = 0; short next = 0; if (len == 1) { return; } short outer_len = (short)(off + len - 1); for (short k = off; k < outer_len; ++k) { short inner_len = (short)(outer_len - (short)(k - off)); for (short i = off; i < inner_len; ++i) { // eliminating an issue that originates from Java-signess, which // leands to a situation where '0x80 (-127)' is less than '0x01'. curr = (short)(buff[i] & 0xFF); next = (short)(buff[(short)(i + 1)] & 0xFF); if (curr <= next) { continue; } temp = (byte)curr; buff[i] = (byte)next; buff[(short)(i + 1)] = temp; } } }декодирование данных, представленных в формате BCD:
public void from_bcd(byte[] buff, short off, short len) { short bound = (short)(off + len); byte _byte = 0; byte msn = 0; byte lsn = 0; for ( ; off < bound; ++off) { _byte = buff[off]; msn = (byte)((_byte & 0xF0) >>> 4); lsn = (byte) (_byte << 4); buff[off] = (byte)(lsn | msn); } // fetch the LSN of the last byte _byte = buff[(short)(off - 1)]; lsn = (byte)(_byte & 0x0F); // discard trailing 'f' (if any) if (lsn == 0x0F) { buff[(short)(off - 1)] = (byte)(_byte & 0xF0); } }
Традиционно, в репозитории уже лежит ant-javacard.jar и его сборочный файл build.xml. Вам только и остается, что открыть консоль в корневой папке проекта и подать команду ant.
В сборочном файле build.xml необходимо указать следующее:
<?xml version="1.0" encoding="UTF-8"?> <project name="Java Card Utils library" default="build-library" basedir="."> <!-- <get src="https://github.com/martinpaljak/ant-javacard/releases/latest/download/ant-javacard.jar" dest="." skipexisting="true"/> --> <taskdef name="javacard" classname="pro.javacard.ant.JavaCard" classpath="ant-javacard.jar"/> <description>GSM library</description> <property name="jc.sdk" value="./libraries/jc304_kit" /> <property name="package.aid" value="A0:00:00:00:85:6C:69:62:75:74:69:6C:73" /> <property name="package.name" value="fun.libutils" /> <property name="package.version" value="1.0" /> <target name="build-library"> <mkdir dir="dist" /> <javacard> <!-- Атрибут verify="false" критически важен для библиотек без Applet.cap --> <cap jckit="${jc.sdk}" aid="${package.aid}" package="${package.name}" version="${package.version}" output="dist/${package.name}.cap" export="dist/" jar="dist/" sources="src/" verify="false"> <!-- Тег <applet> ОПУЩЕН, так как это библиотека --> <!-- Если ваша библиотека зависит от других библиотек, раскомментируйте строку ниже: --> <!-- <import jar="libs/another_ext_lib.jar"/> --> </cap> </javacard> </target> <target name="clean"> <delete dir="dist" /> <mkdir dir="dist" /> </target> </project>
После успешной сборки появится папка dist с несколькими файлами, из которых нас интересуют fun.libutils.cap и libutils.jar. Первый – это модуль, загружаемый в апплет, а последний – библиотека, которую мы подключим к апплету SimpleApplet.java на этапе компиляции.
Апплет SimpleApplet.java
Скачайте проект и откройте один-единственный файл src/simple/applet/SimpleApplet.java. Если вы используете VSCode, то инструкция импорта нашей libutils будет подчеркнута красной кривой – неведомая поеб библиотека:

Для разрешения путей необходимо:
войти в обозреватель проекта;
в нижней части развернуть вкладку «
JAVA PROJECTS»;развернуть вкладку «
SimpleApplet»;навести курсор на вкладку «
Referenced Libraries» - всплывет символ «+» (Add Jar Libraries to Project Classpath) – клацнуть по нему;в открывшемся обозревателе файловой системы перейти в папку
libutils/distи выбратьlibutils.jar.

Далее пройдемся по коду, благо он очень компактный:
package simple.applet; import javacard.framework.*; import fun.libutils.Utils; public class SimpleApplet extends Applet { // константы, обозначающие доступные команды (байт INS заголовка APDU) private static final byte INS_DO_XOR = 0x12; private static final byte INS_BUBBLE_SORT = 0x14; private static final byte INS_PARSE_BCD = 0x16; // Указатель на экземпляр класса импортируемой библиотеки Utils utils; public SimpleApplet() { // Создаем экземпляр класса Utils utils = new Utils(); } public static void install(byte[] bArray, short bOffset, byte bLength) { // Регистрируем наш апплет в JCRE (Java Card Runtime Environment) new SimpleApplet().register(); } public void process(APDU apdu) { // Если прилетела команда "SELECT кого-то там", то надо вернуться // т.к. наш апплет эту команду обрабатывать не умеет и не должен. // JCRE же сама знает, что с ней делать. if (selectingApplet()) { return; } byte[] buff = apdu.getBuffer(); byte cla = buff[ISO7816.OFFSET_CLA]; byte ins = buff[ISO7816.OFFSET_INS]; short lc = (short)(buff[ISO7816.OFFSET_LC] & (short)0xFF); // Наш апплет всегда ждет данные: пустое поле CDATA пакета APDU недопустимо if (lc == 0) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } // Все команды - проприетарные, а мы - прилежные программисты, // а потому, в соответствии с ISO 7816, пропускаем только те команды, // у которых выставлен старший бит поля CLA команды APDU. if (cla != (byte)0x80) { ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); } // Безусловное ожидание входных данных (т.е. поля CDATA) apdu.setIncomingAndReceive(); // В зависимости от значения поля INS выполняем ту или иную команду. switch (ins) { case INS_DO_XOR: { // В каждом кейсе вызывается соответствующий метод из библиотеки libutils.jar utils.xor(buff, ISO7816.OFFSET_CDATA, lc); } break; case INS_BUBBLE_SORT: { utils.bubble_sort(buff, ISO7816.OFFSET_CDATA, lc); } break; case INS_PARSE_BCD: { utils.from_bcd(buff, ISO7816.OFFSET_CDATA, lc); } break; default: ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); } // безусловная отправка ответа. Этот апплет возвращает ровно столько // байт, сколько получил в команде. apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, lc); } }
Резюмируя комментарии к коду еще раз отмечу основные элементы класса SimpleApplet.java:
В конструкторе апплета создается переменная экземпляра:
Utils utils;В методе
void install()регистрируем апплет, чтобыJCREзнала о его существовании, жизненном цикле да и вообще мы могли с ним общаться;В методе
void process()вызываются методы библиотекиUtilsдля обработки входящих APDU-команд.
Библиотека FunGP
Фанаты моего творчества ликуют - в последнем релизе библиотека могёт накатывать либы, а также исправлена ошибка в функции SCP02._retail_mac()! Теперь EMVco может спать спокойно. Ну да хватит лирики, перейдем к делу: скачайте репозиторий и откройте консоль в корневой папке, затем введите:
python -m venv .venv # создаем виртуальное окружение source .venv/bin/activate # активируем его. На Windows venv\Scripts\activate.bat pip install -e . # устанавливаем либу. В процессе будут подтянуты все необходимые зависимости.
Напоминаю, что для корректной работы нужен python 3.12.x. Также вам необходимо скопировать SimpleApplet.cap и fun.libutils.cap в директорию ./resources корневой папки FunGP, т.к. скрипты по их установке и удалению смотрят туда.
Скрипт 01_install_applet.py
from fun_gp import Reader, SmartCard, SCP02, CCM, InstallParams, APPLET_PATH # Ключи ISD isd_keyset = ['404142434445464748494A4B4C4D4E4F', '404142434445464748494A4B4C4D4E4F', '404142434445464748494A4B4C4D4E4F'] # Путь до апплета applet_cap_path = APPLET_PATH / 'SimpleApplet.cap' # Путь до библиотеки lib_cap_path = APPLET_PATH / 'fun.libutils.cap' def install_applet(): with Reader() as reader: # подключаемся к ридеру. Если карта не вставлена, то он будет ждать. # берем указатель на текущую карту isd = SmartCard(reader.plain_apdu, SCP02(isd_keyset), CCM()) isd.transmit('00a4 0400', 0x90, 0x00, 'Select ISD') # выбираем ISD isd.mutual_auth() # проводим взаимную аутентификацию isd.install_lib_scp02(lib_cap_path, 0x90, 0x00) # накатываем библиотеку isd.install_app_scp02(applet_cap_path, InstallParams(), 0x90, 0x00) # накатываем апплет install_applet()
Нас интересуют строки № 18-19, где вызываются методы установки библиотеки и апплета. По здравому разумению становится очевидным, что накатывать апплет с зависимостями, которых нет на карте – бестолковая идея, потому сначала вызывается метод SmartCard.install_lib_scp02():
def install_lib_scp02(self, lib_path:str, exp_sw1:int|None = None, exp_sw2:int|None = None, is_secured=True): """ Install a library by means of SCP02 protocol. :param lib_path: path to a cap file to be installed :param install_params: by default passes applet's AID only. Additional params must be prepended with length value e.g.: `lv(pin) + lv(secret)` so that the resulting string will have the following form: `[len][AID] [len][pin] [len][secret]` """ cap_bytes, pkg_aid, _ = self._ccm.decomposite_cap_file(lib_path) # INSTALL[for load] for_load = self._ccm.make_cmd_install_for_load(pkg_aid, None, LoadParams()) self.transmit(for_load, exp_sw1, exp_sw2, 'INSTALL[for load]', is_secured=is_secured) # LOAD cap_chunks = self._ccm.make_cmd_load(cap_bytes) for chunk in cap_chunks: self.transmit(chunk, exp_sw1, exp_sw2, 'LOAD', is_secured=is_secured) # Note: 'LOAD.Lc1 + LOAD.Lc2 + LOAD.Lcn' is greater than 'self.cap_file_size'. # The difference is C * N + T, where # C - the length of CMAC, # N - number of LOAD commands, # T = C4 BER-TLV object at the beginning of the very first LOAD CDATA field. print(f'***** CAP-file size *****') print(f'\n***** CAP-file parameters *****\n' f'Package AID: {pkg_aid}\n' f'Package size: {self._ccm.cap_file_size} bytes.\n')
И только затем SmartCard.install_app_scp02():
def install_app_scp02(self, cap_path:str, install_params:InstallParams, exp_sw1:int|None = None, exp_sw2:int|None = None, is_secured=True): """ Install an applet through the SCP02 protocol. :param cap_path: path to a cap file to be installed :param install_params: by default passes applet's AID only. Additional params must be prepended with length value e.g.: `lv(pin) + lv(secret)` so that the resulting string will have the following form: `[len][AID] [len][pin] [len][secret]` """ cap_bytes, pkg_aid, app_aid = self._ccm.decomposite_cap_file(cap_path) # INSTALL[for load] for_load = self._ccm.make_cmd_install_for_load(pkg_aid, None, LoadParams()) self.transmit(for_load, exp_sw1, exp_sw2, 'INSTALL[for load]', is_secured=is_secured) # LOAD cap_chunks = self._ccm.make_cmd_load(cap_bytes) for chunk in cap_chunks: self.transmit(chunk, exp_sw1, exp_sw2, 'LOAD', is_secured=is_secured) # INSTALL[for install and make selectable] for_install = self._ccm.make_cmd_install_for_install(pkg_aid, app_aid, install_params) self.transmit(for_install, exp_sw1, exp_sw2, 'INSTALL[for install and make selectable]', is_secured=is_secured) # Note: 'LOAD.Lc1 + LOAD.Lc2 + LOAD.Lcn' is greater than 'self.cap_file_size'. # The difference is C * N + T, where # C - the length of CMAC, # N - number of LOAD commands, # T = C4 BER-TLV object at the beginning of the very first LOAD CDATA field. print(f'***** CAP-file size *****') print(f'\n***** CAP-file parameters *****\n' f'Package AID: {pkg_aid}\n' f'Applet AID: {app_aid}\n' f'Applet size: {self._ccm.cap_file_size} bytes.\n')
Разница между этим методами лишь в том, что для установки библиотеки не требуется команда install[FOR INSTALL & MAKE SELECTABLE].
Давайте протестируем. Для этого в консоли (по идее она у вас сейчас в корневой папке FunGP) перейдите в директорию tests/simple_applet и запустите скрипт:
cd ./tests/simple_applet python 01_install_applet.py Connecting to 'ACS ACR39U ICC Reader 0' reader Command: Select ISD >> 00A40400 00 << 6F108408A000000151000000A5049F6501FF << 9000 [ OK ] duration: 0.06 Command: Initialize update >> 80500000 08 BE6C166A3C279223 << 000023340138302047290102001DA8418EA3C5F15371165DA5141E67 << 9000 [ OK ] duration: 0.11 Key diversification data: 00002334013830204729 KVN and SCP ID : 0102 Key Sequence counter : 001D Card challenge : A8418EA3C5F1 card cryptogram : 5371165DA5141E67 host cryptogram : D28D420BE55BE38E Command: External authenticate >> 84820100 10 D28D420BE55BE38E577A97375F18B217 << 9000 [ OK ] duration: 0.07 Command: INSTALL[for load] >> 84E60200 28 0DA0000000856C69627574696C7300000EEF0CC602FFFFC702FFFFC802FFFF0056622BC7F2268C65 << 00 << 9000 [ OK ] duration: 0.18 Command: LOAD >> 84E80000 FF C482017A010017DECAFFED01020200010DA0000000856C69627574696C7302001F0017001F0000000B0006001000F2000A00050007004300000000000001000004000B01000107A000000062000106001000800000FF00010300000008002900980700F2000110188C00007A04421E1F4129041E2905160516046D121916053E251100FF575B3859050170EC7A03470329040329050329061F046B037A1E1F41044329071E2908160816076D50160716081E434329091E290A160A16096D3919160A251100FF53290519160A0441251100FF532906160516066E04701616055B290419160A16065B3819160A0441160438590A0170C55938C1BDFF7E2330BE << 00 << 9000 [ OK ] duration: 0.4 Command: LOAD >> 84E88001 8F 080170AE7A04441E1F4129040329050329060329071E16046D26191E25290516051100F05307515B29061605074D5B2907191E16071606553859020170D9191E04432529051605100F5329071607100F6B0E191E044316051100F0535B387A08000A000000000000000000000A00070100000001000105000600010680000009000500000001056292E263BAF15EAF << 00 << 9000 [ OK ] duration: 0.41 ***** CAP-file size ***** ***** CAP-file parameters ***** Package AID: A0000000856C69627574696C73 Package size: 378 bytes. Command: INSTALL[for load] >> 84E60200 20 05A00000008400000EEF0CC602FFFFC702FFFFC802FFFF00E76EBF9967DD7FBD << 00 << 9000 [ OK ] duration: 0.15 Command: LOAD >> 84E80000 FF C482018701000FDECAFFED010204000105A00000008402001F000F001F00120025003E000C009B000A00180000007000000000000003010004002503050107A000000062010100010DA0000000856C69627574696C73000107A0000000620001030012010EA00000008453696D706C65417070001206000C00800301000107010000001F07009B000310188C0003188F00013D8C000287007A02308F00043D8C00058B00067A0424188B000760037A198B00082D1A0325321A042529041A07251100FF532905160561081167008D00091F10806A08116E008D0009198B000A3B16047300320012001600110032001C00320027AD001A0873F146875EE631E7 << 00 << 9000 [ OK ] duration: 0.4 Command: LOAD >> 84E88001 9C 16058B000B701EAD001A0816058B000C7013AD001A0816058B000D7008116D008D0009190816058B000E7A08000A0000000000000000000005003E000F020000000181000006810000068003000100000006000001038003010380030303800A010680070103800A0603810001038100020381000303800A080900180004105D0B0B001005040408040307071D0B041D0B0B0807B39300C9B638B711 << 00 << 9000 [ OK ] duration: 0.43 Command: INSTALL[for install and make selectable] >> 84E60C00 34 05A0000000840EA00000008453696D706C654170700EA00000008453696D706C65417070010004C900EF000021FF2BFF2D4A0099 << 00 << 9000 [ OK ] duration: 0.29 ***** CAP-file size ***** ***** CAP-file parameters ***** Package AID: A000000084 Applet AID: A00000008453696D706C65417070 Applet size: 391 bytes. Reader: context has been released.
Теперь целевые тесты на команды XOR, Bubble sort и decode BCD, для этого вбейте нижеследующую команду в консоли:
python 04_simple_applet_test.py Connecting to 'ACS ACR39U ICC Reader 0' reader Command: Select Simple Applet >> 00A40400 0E A00000008453696D706C65417070 << 9000 [ OK ] duration: 0.09 Command: XOR input >> 80120000 06 050403020100 # входные данные << FAFBFCFDFEFF # выходной результат << 9000 [ OK ] duration: 0.05 Command: Bubble sort 128-0 # входные данные >> 80140000 80 807F7E7D7C7B7A797877767574737271706F6E6D6C6B6A696867666564636261605F5E5D5C5B5A595857565554535251504F4E4D4C4B4A494847464544434241403F3E3D3C3B3A393837363534333231302F2E2D2C2B2A292827262524232221201F1E1D1C1B1A191817161514131211100F0E0D0C0B0A090807060504030201 # выходной результат << 0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F80 << 9000 [ OK ] duration: 1.88 Command: parse BCD >> 80160000 03 2143F5 # входные данные << 123450 # выходной результат << 9000 [ OK ] duration: 0.05 Reader: context has been released.
Собственно, вот и все, дорогой читатель. Если у тебя остались вопросы или нашел какую-то ошибку то буду признателен за сообщение в комментах или личке.
Всем пока!

