В статье «Асимметричная криптография в Java Card» был приведен пример по установке защищенного канала в два этапа:

  1. генерация общего секрета по алгоритму ECDH;

  2. использование общего секрета в качестве ключа 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 будет подчеркнута красной кривой – неведомая поеб библиотека:

VSCode не может распознать библиотеку fun.*
VSCode не может распознать библиотеку fun.*

Для разрешения путей необходимо:

  1. войти в обозреватель проекта;

  2. в нижней части развернуть вкладку «JAVA PROJECTS»;

  3. развернуть вкладку «SimpleApplet»;

  4. навести курсор на вкладку «Referenced Libraries» - всплывет символ «+» (Add Jar Libraries to Project Classpath) – клацнуть по нему;

  5. в открывшемся обозревателе файловой системы перейти в папку libutils/dist и выбрать libutils.jar.

1. Explorer; 2. Java Projects; 3. SimpleApplet file heirarchy; 4. Add Jar Libraries.
1. Explorer; 2. Java Projects; 3. SimpleApplet file heirarchy; 4. Add Jar Libraries.

Далее пройдемся по коду, благо он очень компактный:

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:

  1. В конструкторе апплета создается переменная экземпляра: Utils utils;

  2. В методе void install() регистрируем апплет, чтобы JCRE знала о его существовании, жизненном цикле да и вообще мы могли с ним общаться;

  3. В методе 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.

Собственно, вот и все, дорогой читатель. Если у тебя остались вопросы или нашел какую-то ошибку то буду признателен за сообщение в комментах или личке.

Всем пока!