Всем привет!
В статье «Асимметричная криптография в 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, 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 определяет несколько уровней секретности:
Наше ТЗ требует, чтобы секретный ключ передавался на карту в зашифрованном виде. В этой связи мы ставим условие: выбросить исключение если уровень безопасности не 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;}
Он прост как точильный камень:
-
переводим шифратор в режим шифрования;
-
шифруем данные;
-
возвращаем данные.
Так, с апплетом закончили, осталось лишь скомпилировать его, затем перекинуть файл в 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/