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

—

от автора

Всем привет!

В статье «Асимметричная криптография в JavaCard» приводится пример создания канала защищенного обмена сообщениями на сессионных симметричных ключах, вырабатываемых по протоколу ECDH. У этого решения есть два спорных момента: во-первых, сама реализация потребует дополнительных ресурсов на тщательное тестирование, а во-вторых ECDH не обеспечивает аутентификацию сторон. Этот пункт более актуален, т.к. без поддержки апплетом инфраструктуры открытых ключей (PKI) и цифровых сертификатов, он выдаст секреты любому, кто знает параметры эллиптической кривой. Т.е. нужна еще куча времени на разработку и отладку. Преодолеть эти сложности можно одним элегантным решением: использовать механизмы безопасности, предоставляемые «главным по карте» — Issuer Security Domain.

Исходники: ISD_secured_applet, FunGP

Суть задачи

Представим, что на предприятии ввели систему СКУД и вход в здание только по картам, на которых хранится информация о сотруднике: ФИО, департамент, должность и срок действия карты. Записать/обновить данные вправе лишь авторизированный пользователь и строго по защищенному каналу. Считать же может любой СКУД, однако получит он их в зашифрованном виде на алгоритме AES-128. Это значит, что нам надо как-то передать карте 16-байтовый ключ и сделать это максимально безопасно. Также есть необходимость в периодическом обновлении ключа в течение всего жизненного цикла карты.

Решение: ISD_secured_applet.java

Итак, видим цель – не видим препятствий. Начнем описание с базовых элементов апплета и первым на очереди конструктор:

publicISD_secured_applet(){    person_info = new byte[_255];}

Несмотря на его лаконичность, именно на нем я столкнулся с очередным подвохом. Суть дела в том, что до сих пор апплеты для статей гонялись на симуляторе, который без проблем проглатывал создание внутри конструктора таких тяжеловесных объектов, как javacard.security.AESKey, javacard.security.KeyAgreement и прочих. А вот на этот раз решил прогнать тесты на двух новеньких картах от NXP и получал жесткий бабах: на команде INSTALL[for install & make selectable] возникала ошибка SW 6F00. Первое (и единственное), что приходило на ум, это отсутствие поддержки криптографии. Затем был изнурительный гуглежь, который предложил инстанциировать супертяжей в отдельных функциях уже после инсталляции апплета, что и оказалось решением проблемы.

Но продолжим исследования. Экземпляр класса создается внутри метода install():

public static voidinstall(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 voidprocess(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 booleanselect(){    scp02 = GPSystem.getSecureChannel();    return true;}public voiddeselect(){    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 shortmutual_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 shortupdate_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 Card Specification, v. 2.3, appendix E "Secure Channel Protocol 02"

GlobalPlatform Card Specification, v. 2.3, appendix E «Secure Channel Protocol 02»

Согласно спецификации 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 levelif ((sec_level & SecureChannel.C_DECRYPTION) != SecureChannel.C_DECRYPTION) {            SOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);}

GlobalPlatform определяет несколько уровней секретности:

GlobalPlatform Card Specification, v. 2.3, appendix E.5.2.3 "Reference Control Parameter P1"

GlobalPlatform Card Specification, v. 2.3, appendix E.5.2.3 «Reference Control Parameter P1»

Наше ТЗ требует, чтобы секретный ключ передавался на карту в зашифрованном виде. В этой связи мы ставим условие: выбросить исключение если уровень безопасности не C_DECRYPTION (толкуется как «команды Хоста в зашифрованном виде»). Далее создаем экземпляры AESKey и Cipher, затем записываем ключевой материал.

Метод set_person_info(byte[] buffer, shortapdu_len) аналогичен предыдущему с той лишь разницей, что предъявляет куда меньшие требования к минимальному уровню защиты (C_MAC, «команды хоста должны быть защищены хотя бы имитовставкой»).

Чтение данных с карты

Мы вплотную подобрались к финальной части апплета — чтение персональных данных:

protected shortget_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;}

Он прост как точильный камень:

  1. переводим шифратор в режим шифрования;

  2. шифруем данные;

  3. возвращаем данные.

Так, с апплетом закончили, осталось лишь скомпилировать его, затем перекинуть файл в isd_secured_applet.cap FunGP/resources:

PS C:\_JavaCard-dev\ISD_secured_applet> antBuildfile: C:\_JavaCard-dev\ISD_secured_applet\build.xmlbuild-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.capBUILD SUCCESSFULTotal 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.pyConnecting to 'ACS ACR39U ICC Reader 0' readerCommand: Select ISD secured applet>> 00A4 0400 10 A0000000864953442053656375726564<< 9000 [OK ]duration: 0.11Command: Get personal data>> 0022 0000 00<< 21248845A85A26ADC83E1D584212A667F45DE3DA777E2C2D67647B1E1C36DAB9E60EC482FE97BC7DC9B9247A05CBC7FD9A8117161FAA1F9AC8FB7FCBA2F26D0B784376FE6DE9FF465CA0A98881E6FE722D1D9137F14EDC47CA870BDD089F83D651DE339E80107F3559CCB5FA85B8411AA325EBAC4DE494831043998283FB69020FC5F29E66FFF1AB645B56D591F09272B7A5DBA64811F62D0C0446886633CF60FE15FC14DBD65FD7F336FF8FC7855BBD9332D23BD5BBB26DF1813920EA48A04ED36F3AFE368D40F35880CC6B4E8C14AE1E40CF6A8F72E5E197147EC44FD60E68BF0D3DCF1A62C762D935839CCF2E6B93<< 9000 [OK ]duration: 0.25                        ***ДАННЫЕ ДЕРЖАТЕЛЯ КАРТЫ***        | Фамилия         | Исламов                          |        | Имя             | Тельман                          |        | Отчество        | Исламович                        |        | Департамент     | Департамент IT продуктов         |        | Отдел           | Отдел разработки дополнительных сервисов |        | Должность       | Ведущий инженер                  |        | Действителен до | 05.10.2036                       |Reader: context has been released.

В принципе, на этом повествование окончено, дорогой читатель. Скорее всего у тебя осталось легкое чувство недосказанности, некий вопрос повис в воздухе, типа: «какого хрена мы городим AES шифрование для защиты выходных данных, если можно было бы обойтись тем же SCP02?!». Понимаю, но к сожалению этот протокол не поддерживает шифрование выходных данных. Он изначально проектировался как механизм защищенной записи. Чтобы покрыть этот недостаток был введен протокол SCP03.

Всем пока!

ссылка на оригинал статьи https://habr.com/ru/articles/1090692/