Используем ISD для защищенной персонализации Java Card апплета

В статье «Асимметричная криптография в JavaCard» приводится пример создания канала защищенного обмена сообщениями на сессионных симметричных ключах, вырабатываемых по протоколу ECDH. У этого решения есть два спорных момента: во-первых, сама реализация потребует дополнительных ресурсов на тщательное тестирование, а во-вторых ECDH не обеспечивает аутентификацию сторон. Этот пункт более актуален, т.к. без поддержки апплетом инфраструктуры открытых ключей (PKI) и цифровых сертификатов, он выдаст секреты любому, кто знает параметры эллиптической кривой. Т.е. нужна еще куча времени на разработку и отладку. Преодолеть эти сложности можно одним элегантным решением: использовать механизмы безопасности, предоставляемые «главным по карте» - Issuer Security Domain.
Исходники: ISD_secured_applet, FunGP
Суть задачи
Представим, что на предприятии ввели систему СКУД и вход в здание только по картам, на которых хранится информация о сотруднике: ФИО, департамент, должность и срок действия карты. Записать/обновить данные вправе лишь авторизированный пользователь и строго по защищенному каналу. Считать же может любой СКУД, однако получит он их в зашифрованном виде на алгоритме AES-128. Это значит, что нам надо как-то передать карте 16-байтовый ключ и сделать это максимально безопасно. Также есть необходимость в периодическом обновлении ключа в течение всего жизненного цикла карты.
Решение: ISD_secured_applet.java
Итак, видим цель – не видим препятствий. Начнем описание с базовых элементов апплета и первым на очереди конструктор:
public
ISD_secured_applet()
{
person_info = new byte[_255];
}
Несмотря на его лаконичность, именно на нем я столкнулся с очередным подвохом. Суть дела в том, что до сих пор апплеты для статей гонялись на симуляторе, который без проблем проглатывал создание внутри конструктора таких тяжеловесных объектов, как javacard.security.AESKey, javacard.security.KeyAgreement и прочих. А вот на этот раз решил прогнать тесты на двух новеньких картах от NXP и получал жесткий бабах: на команде INSTALL[for install & make selectable] возникала ошибка SW 6F00. Первое (и единственное), что приходило на ум, это отсутствие поддержки криптографии. Затем был изнурительный гуглежь, который предложил инстанциировать супертяжей в отдельных функциях уже после инсталляции апплета, что и оказалось решением проблемы.
Но продолжим исследования. Экземпляр класса создается внутри метода install():
public static void
install(byte[] buffer, short offset, byte length)
{
// the the length of instance AID.
length = buffer[offset++]; // The 'offset' will point to the AID.
new ISD_secured_applet().register(buffer, offset, length);
}Обратите внимание, что в метод register() передается AID экземпляра апплета. Делается это по строгой рекомендации параграфа «6.1 install() method» документа «Guidelines for Developing Java Card Applications on GlobalPlatform Cards», v1.0 от 2002 года. Господи, я был в 10-м классе. Но документ занятный, рекомендую к прочтению.
Теперь метод process():
public void
process(APDU apdu) throws ISOException
{
if (selectingApplet()) {
return;
}
byte[] buffer = apdu.getBuffer();
byte ins = buffer[ISO7816.OFFSET_INS];
short len = _00;
try {
if (has_cdata(ins)) {
len = apdu.setIncomingAndReceive();
if (len != apdu.getIncomingLength()) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
}
switch (buffer[ISO7816.OFFSET_INS]) {
case INS_SET_SECRET_KEY: {
len = update_secret_key(buffer, len);
} break;
case INS_GET_PERSONAL_INFO: {
len = get_person_info(buffer, len);
} break;
case INS_SET_PERSONAL_INFO: {
len = set_person_info(buffer, len);
} break;
default:
len = mutual_authentication(apdu);
}
if (len > _00) {
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
}
} catch (ArrayIndexOutOfBoundsException e) {
ISOException.throwIt(SW_ARRAY_INDEX_OUT_OF_RANGE);
} catch (CryptoException e) {
short reason = (short)(e.getReason() & (short)0x00FF);
ISOException.throwIt((short)(SW_CRYPTO_EXCEPTION | reason));
}
}Здесь обрабатываются команды, поступающие апплету: запись секретного ключа, внесение и чтение персональных данных. Отдельно упомяну логику блока
if (has_cdata(ins)) {
len = apdu.setIncomingAndReceive();
if (len != apdu.getIncomingLength()) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
}Во время разработки архитектуры необходимо определиться c неким протоколом, который «заявит твердо и четко», что update_secret_key() и set_person_info() несут полезную нагрузку (CDATA), а get_person_info() – нет. За это отвечает метод has_cdata(), который скажет апплету, стоит ли вызвать метод apdu.setIncomingAndReceive() или нет.
Также обратите внимание на блок try-catch, который обрабатывает исключения, связанные с криптографией и выходом за границы массива. Без него эти ошибки будут выглядеть в консоли как «6F00», т.е. крайне неинформативно. А благодаря блоку
catch (CryptoException e) {
short reason = (short)(e.getReason() & (short)0x00FF);
ISOException.throwIt((short)(SW_CRYPTO_EXCEPTION | reason));
}мы будем четко понимать, какая именно причина прервала работу:
//********** класс CryptoException.java **********//
package javacard.security;
import javacard.framework.CardRuntimeException;
public class CryptoException extends CardRuntimeException {
public static final short ILLEGAL_VALUE = 1;
public static final short UNINITIALIZED_KEY = 2;
public static final short NO_SUCH_ALGORITHM = 3;
public static final short INVALID_INIT = 4;
public static final short ILLEGAL_USE = 5;
public CryptoException(short var1) {
super(var1);
}
public static void throwIt(short var0) {
}
}
Мы постепенно подбираемся к сути: класс org.globalplatform.SecureChannel.
Наш апплет переопределяет методы select() и deselect() таким образом, чтобы при селектировании заполучить экземпляр этого класса и высвободить этот ресурс при выходе.
public boolean
select()
{
scp02 = GPSystem.getSecureChannel();
return true;
}
public void
deselect()
{
scp02.resetSecurity();
}
Обратите внимание, что select() всегда возвращает ‘true’ – это тоже из рекомендаций «Guidelines for Developing».
Взаимная аутентификация между хостом и апплетом
Давайте более детально взглянем на блок switch-case в методе process():
switch (buffer[ISO7816.OFFSET_INS]) {
case INS_SET_SECRET_KEY: {
len = update_secret_key(buffer, len);
} break;
case INS_GET_PERSONAL_INFO: {
len = get_person_info(buffer, len);
} break;
case INS_SET_PERSONAL_INFO: {
len = set_person_info(buffer, len);
} break;
default:
len = mutual_authentication(apdu);
}Здесь обрабатываются три проприетарные команды, а все прочие идут лесом в mutual_authentication():
protected short
mutual_authentication(APDU apdu)
{
short resp_len = scp02.processSecurity(apdu);
return resp_len;
}В описании метода SecureChannel.processSecurity() (GlobalPlatform Card API 1.5) сказано, что его задача – максимально абстрагировать апплет от механизмов безопасности родительского домена. Реализация строится на предположении, что любая команда, которая неизвестна апплету (потому-то он и вызывается из блока ‘default’), относится вопросам безопасности. В нашем случае сюда попадают INITIALIZE UPDATE и EXTERNAL AUTHENTICATE для выставления требуемого уровня безопасности. Если же команда неизвестна, то будет выброшено исключение SW_CLA_NOT_SUPPORTED или SW_INS_NOT_SUPPORTED.
Запись данных по SCP02
Посмотрите на метод update_secret_key():
protected short
update_secret_key(byte[] buffer, short apdu_len)
{
apdu_len = scp02.unwrap(buffer, ISO7816.OFFSET_CLA, (short)(apdu_len + 5));
apdu_len -= 5;
byte sec_level = scp02.getSecurityLevel();
// enforce applying C_DECRYPTION security level
if ((sec_level & SecureChannel.C_DECRYPTION) != SecureChannel.C_DECRYPTION) {
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
}
if (aes_key == null) {
aes_key = (AESKey)KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_128, false);
aes_cipher = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2, false);
}
aes_key.setKey(buffer, ISO7816.OFFSET_CDATA);
return _00;
}Первое, на что надо обратить внимание – это вызов метода SecureChannel.unwrap(). Тут потребуется небольшое отступление. Протокол SCP02 имеет т.н. параметр «i», определяющий поддерживаемые форматы криптографической защиты:

Согласно спецификации GlobalPlatform, appendix ‘E.1.1’, GP-совместимая имплементация должна поддерживать как минимум следующие опции:
i = '15'i = '1A'i = '55' <-наш случай (card challenge | карта не подписывает свои APDU | дополнительное шифрование C-MAC | нулевой ICV | явная инициация ЗОС | C-MAC рассчитывается отCLA INS P1 P2 Lc CDATA [Padding]| используются три ключа)
Так вот, когда сюда прилетает APDU, этот метод «распаковывает» его: расшифровывает и проверяет MAC. Заметили, что аргумент последнего параметра SecureChannel.unwrap() имеет значение (apdu_len + 5)? Будьте внимательны, иначе как и я будете часами отлавливать этих «блох», пока не поймете, что отступ ISO7816.OFFSET_CLA «подъедает» последние пять байт поля CDATA.
Далее идет очень важный блок:
byte sec_level = scp02.getSecurityLevel();
// enforce applying C_DECRYPTION security level
if ((sec_level & SecureChannel.C_DECRYPTION) != SecureChannel.C_DECRYPTION) {
SOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
}GlobalPlatform определяет несколько уровней секретности:

Наше ТЗ требует, чтобы секретный ключ передавался на карту в зашифрованном виде. В этой связи мы ставим условие: выбросить исключение если уровень безопасности не C_DECRYPTION (толкуется как «команды Хоста в зашифрованном виде»). Далее создаем экземпляры AESKey и Cipher, затем записываем ключевой материал.
Метод set_person_info(byte[] buffer, shortapdu_len) аналогичен предыдущему с той лишь разницей, что предъявляет куда меньшие требования к минимальному уровню защиты (C_MAC, «команды хоста должны быть защищены хотя бы имитовставкой»).
Чтение данных с карты
Мы вплотную подобрались к финальной части апплета - чтение персональных данных:
protected short
get_person_info(byte[] buffer, short apdu_len)
{
aes_cipher.init(aes_key, Cipher.MODE_ENCRYPT);
short out_data_len = aes_cipher.doFinal(person_info, _00, person_info_len, buffer, ISO7816.OFFSET_CDATA);
return out_data_len;
}Он прост как точильный камень:
переводим шифратор в режим шифрования;
шифруем данные;
возвращаем данные.
Так, с апплетом закончили, осталось лишь скомпилировать его, затем перекинуть файл в isd_secured_applet.cap FunGP/resources:
PS C:\_JavaCard-dev\ISD_secured_applet> ant
Buildfile: C:\_JavaCard-dev\ISD_secured_applet\build.xml
build-applet:
[cap] INFO: using JavaCard 3.0.4 SDK in C:\_JavaCard-dev\ISD_secured_applet\libraries\jc304_kit with JDK 8
[cap] INFO: Setting package name to fun.isd_secured_applet
[cap] Building CAP with 1 applet from package fun.isd_secured_applet (AID: A000000086)
[cap] fun.isd_secured_applet.ISD_secured_applet A0000000864953442053656375726564
[compile] Compiling files from C:\_JavaCard-dev\ISD_secured_applet\src
[verify] Verification passed
[cap] CAP saved to C:\_JavaCard-dev\ISD_secured_applet\out\isd_secured_applet.cap
BUILD SUCCESSFUL
Total time: 0 seconds
FunGP: тестируем апплет
Фанаты FunGP неистовствуют: теперь библиотека поддерживает несколько уровней безопасности, которые можно легко и непринужденно задать на этапе взаимной аутентификации. Ура!
Ну да хватит лирики скачайте и установите библиотеку:
python -m venv .venv # создаем виртуальное окружение python:
.venv\Scripts\activate.bat # активируем его (Windows)
source .venv/bin/activate # активируем его (Ubuntu)
pip install -e . # локальная установка с подтягиванием зависимостей
mkdir resources # создаем папку, в которой будет храниться .cap-файлСейчас вернитесь в проект ISD_secured_applet и скопируйте isd_secured_applet.cap в папку ‘FunGP/resources’:
cp .\out\isd_secured_applet.cap ..\..\FunGP\resources\Отлично, теперь давайте накатим апплет на карту, для этого перейдите в папку FunGP/tests/isd_secured_applet и запустите скрипт 01_install_applet.py:
def install_applet():
sec_level = SecurityLevel.C_MAC
with Reader() as reader:
isd = SmartCard(reader.plain_apdu, SCP02(isd_keyset), CCM())
isd.transmit('00a4 0400', 0x90, 0x00, 'Select ISD')
isd.mutual_auth(security_level=sec_level)
isd.install_app_scp02(applet_cap_path, InstallParams(),exp_sw1=0x90, exp_sw2=0x00)
install_applet()Вы можете «поиграть» с параметром ‘sec_level’, например, задать уровень C_DECRYPTION и тогда все команды будут переданы в зашифрованном виде.
Далее откройте файл 03_set_aes128_key.py:
def store_secret(isd:SmartCard, msg:bytes, coding:str='latin-1'):
cdata = msg
isd.transmit('8020 0000' + lv_hex(cdata), 0x90, 0x00, 'Store the AES key')
print(f"Secret key: set\n")
def initialize_applet():
sec_level = SecurityLevel.C_DECRYPTION
aes_16_key = hex_to_bytes("0102030405060708 0102030405060708")
with Reader() as reader:
isd = SmartCard(reader.plain_apdu, SCP02(isd_keyset), CCM())
isd.transmit('00a4 0400' + lv_hex('A000000086 4953442053656375726564'), 0x90, 0x00, cmd_name='Select ISD secured applet')
isd.mutual_auth(security_level=sec_level)
store_secret(isd, aes_16_key)Обратите внимание, что мы селектируем наш апплет и далее осуществляем процедуру взаимной аутентификации. Да-да, здесь апплет начнет обрабатывать команды в опции default: mutual_authentication(apdu);
Примечательно, что именно сейчас хост и карта договариваются об уровне безопасности. Для интереса измените параметр sec_level с «C_DECRYPTION» на «C_MAC» и на следующем шаге «store_secret(isd, aes_16_key)» вы выхватите ошибку SW 6982 (SecurityConditionsnotsatisfied) – это сработает наше условие внутри метода update_secret_key(byte[] buffer, short apdu_len).
А вот в скрипте 04_write_card_holder_data.py:
def initialize_applet():
sec_level = SecurityLevel.C_MAC
coding = 'utf-16-be'
with Reader() as reader:
isd = SmartCard(reader.plain_apdu, SCP02(isd_keyset), CCM())
isd.transmit('00a4 0400' + lv_hex('A000000086 4953442053656375726564'), 0x90, 0x00, cmd_name='Select ISD secured applet')
isd.mutual_auth(security_level=sec_level)
set_peronal_info(isd, coding)подобной проблемы не будет – вы можете смело сменить значение аргумента sec_level с C_MAC на C_DECRYPTION т.к. уровень безопасности можно повышать без каких-либо опасений.
Ну и финальный скрипт 05_read_card_holder_data.py заполучит от карты данные держателя в зашифрованном виде, расшифрует их и выведет на дисплей:
05_read_card_holder_data.py
Connecting to 'ACS ACR39U ICC Reader 0' reader
Command: Select ISD secured applet
>> 00A4 0400 10 A0000000864953442053656375726564
<< 9000 [OK ]
duration: 0.11
Command: Get personal data
>> 0022 0000 00
<< 21248845A85A26ADC83E1D584212A667F45DE3DA777E2C2D67647B1E1C36DAB9E60EC482FE97BC7DC9B9247A05CBC7FD9A8117161FAA1F9AC8FB7FCBA2F26D0B784376FE6DE9FF465CA0A98881E6FE722D1D9137F14EDC47CA870BDD089F83D651DE339E80107F3559CCB5FA85B8411AA325EBAC4DE494831043998283FB69020FC5F29E66FFF1AB645B56D591F09272B7A5DBA64811F62D0C0446886633CF60FE15FC14DBD65FD7F336FF8FC7855BBD9332D23BD5BBB26DF1813920EA48A04ED36F3AFE368D40F35880CC6B4E8C14AE1E40CF6A8F72E5E197147EC44FD60E68BF0D3DCF1A62C762D935839CCF2E6B93
<< 9000 [OK ]
duration: 0.25
***ДАННЫЕ ДЕРЖАТЕЛЯ КАРТЫ***
| Фамилия | Исламов |
| Имя | Тельман |
| Отчество | Исламович |
| Департамент | Департамент IT продуктов |
| Отдел | Отдел разработки дополнительных сервисов |
| Должность | Ведущий инженер |
| Действителен до | 05.10.2036 |
Reader: context has been released.В принципе, на этом повествование окончено, дорогой читатель. Скорее всего у тебя осталось легкое чувство недосказанности, некий вопрос повис в воздухе, типа: «какого хрена мы городим AES шифрование для защиты выходных данных, если можно было бы обойтись тем же SCP02?!». Понимаю, но к сожалению этот протокол не поддерживает шифрование выходных данных. Он изначально проектировался как механизм защищенной записи. Чтобы покрыть этот недостаток был введен протокол SCP03.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.