В этой статье разговор пойдет о приложении на Java Card. На хабре уже есть несколько публикаций на эту тему, в своей же я бы хотел немного выйти за рамки стандартного “Hello World”. Здесь вы узнаете как собирать апплет, увидите пример размещения данных в апплет на этапе инсталляции, а также установим защищенный канал между Хостом и картой.
Апплет и библиотека тестирования доступны на github.
Легенда гласит, что отцы-основатели загорелись идеей языка, способного избавить программистов от извечных проблем с утечкой памяти и прочих соотносящихся непотребств. Сказано – сделано: так появилась сложная система под названием Java Card platform (JCp), которая включает в себя виртуальную машину (Java Card Virtual Machine, JCVM) и среду исполнения (Java Card Runtime Environment, JCRE). Однако, JCp это не просто попытка спрятать острые указатели в мягкие ножны, она еще старательно придерживается многочисленных требований, прописанных в смежных стандартах и спецификациях (Global Platform, ISO 7816, ETSI), и делает это так усердно, что намертво закрепилась на многомиллиардном рынке идентификационных документов, банковских карт и SIM-карт.
Тем не менее за все надо платить, а когда у тебя микроконтроллер с парой десятков килобайт SRAM, львиная доля которых уходит на работу самой JCp, то платить приходится больше, чем обычно.
Первая боль – это увесистые апплеты с функционалом «хрен, да ни хрена», а памяти нормально-так поджирают. Например, наш «подопытный» выполняет лишь четыре функции, а весит 1,8 Кбайт. Что-то мне подсказывает, что на «Си» я бы справился с куда меньшими тратами.
Вторая боль – это нестандартная работа с оперативной памятью: у тебя есть право создать массив волатильной памяти на этапе инсталляции, вот им и пользуйся. Только помни, что в распоряжении, скорее всего, не более 600 байт SRAM, потому позаботься о будущих соседствующих проектах, которые банально не пройдут по оперативке.
Ну и куда же без оператора «new»: в Java, C#, С++ и Си (только там аналогом выступает malloc()) выделение памяти происходит в SRAM. «Ну, в принципе, а где же еще?» — спросит внимательный читатель — «Подержи моё пиво» — ответит JCRE, выделяя память во FLASH. Держа в уме весь этот багаж рекомендую не тратить время на бенчмаркинг.
Но вернемся к нашему агнцу. Убедившись, что на вашей машине бегает Java 8, в корневой папке проекта откройте терминал и введите команду ant, которая должна выдать нижеследующий лог:
$ ant # UbuntuBuildfile: /home/telman/develop/SecMsgApplet/build.xmldist: [cap] INFO: using JavaCard 3.0.4 SDK in /home/telman/develop/SecMsgApplet/libraries/jc304_kit with JDK 8 [cap] INFO: Setting package name to secure.messaging [cap] Building CAP with 1 applet from package secure.messaging (AID: A000000083) [cap] secure.messaging.Main A0000000837365636D7367 [compile] Compiling files from /home/telman/develop/SecMsgApplet/src [compile] Compiling 2 source files to /tmp/jccpro8021669146027107305 [compile] /home/telman/develop/SecMsgApplet/src/secure/messaging/ECCurves.java [compile] /home/telman/develop/SecMsgApplet/src/secure/messaging/Main.java [verify] Verification passed [cap] CAP saved to /home/telman/develop/SecMsgApplet/SM_applet.capBUILD SUCCESSFULTotal time: 0 seconds
Как результат, в корневой директории появится файл «SM_applet.cap». Так, со сборкой разобрались, теперь давайте посмотрим, что мы собираем.
Апплет: метод install()
В обязанности JCp входят задачи по администрированию апплета на всех этапах его жизненного цикла — с момента установки и вплоть до удаления. Сущность, которая отвечает за эту часть работы именуется JCRE. Вот именно она и дергает метод install(), чтобы создать экземпляр класса.
public static voidinstall(byte[] params, short off, byte len){ Main applet = new Main(params, off, len); applet.register();}
Внутри этого метода создается экземпляр нашего апплета, в конструктор которого передается массив с пользовательскими данными.
publicMain(byte[] params, short off, byte len){ram_buff = JCSystem.makeTransientByteArray(_64, JCSystem.CLEAR_ON_RESET);state = JCSystem.makeTransientByteArray(_01, JCSystem.CLEAR_ON_RESET);secret = new byte[_32];len = params[off++];// Instance AID lengthoff += len;len = params[off++];// Control info lengthoff += len;len = params[off++];// applet data length (i.e. C9xx)if (len != _32) {ISOException.throwIt((short)(ISO7816.SW_WRONG_LENGTH | _01));}Util.arrayCopy(params, off, secret, _00, len);ecc_keys = ECCurves.getKeyPair();ecc_keys.genKeyPair();private_key = (ECPrivateKey)ecc_keys.getPrivate();public_key = (ECPublicKey)ecc_keys.getPublic();agreement = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false);agreement.init(private_key);aes_key = (AESKey)KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_RESET, KeyBuilder.LENGTH_AES_128, false); aes_cipher = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2, false);state[_00] = STATE_APDU_PLAIN;}
Из интересного тут:
-
метод
JCSystem.makeTransientByteArray()создаст массив типаbyteи сохранит его во FLASH, но сам буфер будет расположен в SRAM, таким образом у нас есть 64 байта для работы с промежуточными значениями. -
Строки
len = params[off++]; off += len;перепрыгивают через данные, которые прописаны в стандарте GlobalPlatform v2.3 тут:

тут:

и тут (JCRE specification, version 3.0.4):

Конкретно в нашем примере мы перепрыгиваем через AID нашего экземпляра (если вообще передавали), через control info — хрен пойми что такое implementation dependent; и попадаем на applet data, которые идентифицируются тегом C9. Важно помнить, что их длина не должна превышать 127 байт.
Принятые 32 байта данных мы сохраним в массив с говорящим именем «secret». Далее создаем:
-
Ассиметричную ключевую пару на эллиптической кривой
secP256k1; -
экземпляр класса
KeyAgreement(он нам понадобится для генерации сессионных ключей по алгоритму ECDH); -
Экземпляр класса, реализующего AES шифрование;
-
Экземпляр класса, представляющего ключ AES.
Непосредственно перед выходом задаем состояние апплета как «APDU PLAIN», т.е. защищенный канал не установлен. И финальный штрих – это вызов метода register(), наследуемый нашим апплетом от своего родителя javacard.framework.Applet. Он критически необходим, т.к. информирует JCRE о том, что экземпляр апплета успешно создан и его необходимо зарегистрировать, любить и беречь.
Апплет: метод process()
Этот метод, также наследуемый от класса Applet, осуществляет взаимодействие между апплетом и Хостом. Первым делом в нем проводится проверка на команду SELECT. Дело в том, что, когда мы селектируем наш апплет, JCRE в своих реестрах ставит напротив него галочку «текущий», соответственно, все команды передаются ему. Если прилетит команда 00A4 04 00 (SELECT ISD) — а обработчика этой команды у нас в switch-case нет — то наш метод вернет ошибку, т.к. не знает, что это за команда такая.
Далее мы работаем с APDU, который имеет следующую структуру:
|CLA| |INS| |P1 | |P2 | |Lc | [CDATA 0:255];
Главное при работе с APDU — это разобраться, какие команды имеют поле CDATA, а какие нет. Для этого у нас имеется вспомогательный метод
private booleanhas_cdata(byte ins){ boolean result; switch (ins) { case INS_GENERATE_SHARED: case INS_CIPHERING: case INS_STORE_SECRET: result = true; break; default: result = false; } return result;}
Если команда находится в этом списке, то апплет будет ждать байты в CDATA. Помните, что ключи от светофора у вас, потому сами решайте какая команда попадет в этот список, а какая — нет. На этапе тестирования мы напишем тест конкретно под этот метод, будет интересно, не переключайтесь.
Вернемся в process(). Если метод has_cdata() вернул true, тогда apdu.setIncomingAndReceive() запросит у JCp процедуру приема байтов CDATA. Затем, выражение len != apdu.getIncomingLength() проверяет, равно ли количество полученных байтов заявленным в поле Lc заголовка APDU.
Блок switch-case мы еще подробно разберем, пока же необходимо закончить повествование о process(), в конце которого есть две важные составляющие. Первая из них это проверка на наличие исходящих данных:
if (len > _00) { apdu.setOutgoingAndSend(_00, len);}
Ее задача — дать указание JCp отправить ответную APDU (R-APDU) в случае необходимости.
Вторая важная деталь – это перехват исключений. Как вы помните из начала повествования, JCVM – это подмножество JVM, а значит может выбрасывать такие исключения, как: ArrayIndexOutOfBoundsException, CryptoException, APDUException и прочие. Из всего этого многообразия стандарт ISO 7816 разрешает лишь ISOException. Если ваш апплет упал с ошибкой “6F00” то скорее всего вы что-то где-то недоглядели. Вот вам лопата — копайте. В этой связи, если мы получили выход за границы массива, то его необходимо перехватить, а вместо него выбросить ISOException с проприетарным кодом ошибки (SW1, SW2).
Апплет: метод generate_ecdh_shared_secret()
В текущей реализации метод KeyAgreement.generateSecret() требует на вход открытый ключ со следующими качествами:
-
несжатый формат;
-
длина 65 байт, в т.ч. префиксный маркер формата «0x04»;
-
точка не должна быть бесконечно удаленной.
Примерный вид: [0x04, 0x01, 0x02, .., 0x3F, 0x40]. Вооружившись этими требованиями, расставляем соответствующие проверки внутри метода:
// Validate input length (uncompressed EC point = 65 bytes)if (lc != SIZE_EC_POINT) { ISOException.throwIt(SW_WRONG_LENGTH);}// Validate point format (must be uncompressed: 0x04)if (buff[off] != (byte) 0x04) { ISOException.throwIt(SW_WRONG_DATA);}// Validate that point is not at infinity (all zeros)for (short i = 1; i < SIZE_EC_POINT; i++) { if (buff[(short) (off + i)] != (byte) 0x00) { allZeros = false; break; }}if (allZeros) { ISOException.throwIt(SW_WRONG_DATA);}
Если все ОК, то вызываем
le = agreement.generateSecret(buff, off, lc, ram_buff, 00);
который возьмет открытый ключ из buff длиною lc по отступу off, а общий секрет сохранит в rambuff по отступу _00.
Далее инициализируем AES ключ первыми 16 байтами общего секрета:
aes_key.setKey(ram_buff, 00);
Кстати, обратите внимание на инструкцию создания aeskey в конструкторе класса:
aes_key = (AESKey)KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_RESET, KeyBuilder.LENGTH_AES_128, false);
Аргумент KeyBuilder.LENGTH_AES_128’ гласит, что длина ключа 16 байт, а KeyBuilder.TYPE_AES_TRANSIENT_RESET дает указание JCRE хранить его в SRAM.
Иные доступные опции:
-
TYPE_AES_TRANSIENT_DESELECT– то же, что и предыдущий, только затирается при выборе другого апплета (а не по сбросу питания); -
TYPE_AES– хранение во FLASH;
Последние три действия, требующие внимания, это:
-
le = public_key.getW(buff, _00)– кладем открытый ключ карты в буфер APDU; -
Util.arrayFillNonAtomic(ram_buff, 00, 32, (byte)_00)– затираем ram_buff в котором хранился общий секрет; -
state[_00] = STATE_APDU_SECURED– выставляем флаг, оповещающий об установке защищенного канала.
Апплет: метод ciphering()
Очень простой и лаконичный метод, который в зависимости от параметра P1 заголовка APDU выполняет операцию шифрования или дешифрования:
private shortciphering(byte[] buff, short off, short lc, byte p1){if (p1 != Cipher.MODE_ENCRYPT && p1 != Cipher.MODE_DECRYPT) {ISOException.throwIt(SW_INCORRECT_P1P2);}aes_cipher.init(aes_key, p1);Util.arrayCopyNonAtomic(buff, off, ram_buff, _00, lc);lc = aes_cipher.doFinal(ram_buff, _00, lc, buff, _00);return lc;}
Проверка параметра P1 проследит, чтобы в метод Cipher.init() был передано одно из двух допустимых значений. Далее инициализируем шифратор актуальным ключом и выполняем целевую операцию.
Оставшиеся два метода fetch_secret() и store_secret() делают одну и ту же работу и отличаются лишь полярностью:
-
Проверка наличия защищенного канала;
-
Длина данных строго 32 байта;
-
Инициализация AES-шифратора;
-
Целевая операция.
С апплетом, вроде, разобрались, двигаемся в сторону библиотеки тестирования.
библиотека FunGP
Мои преданные фанаты (а если вы не из их числа, то бросайте все и скорей вступайте в их ряды) в курсе, что я пишу библиотеку для тестирования смарт-карт, которая бегает на python 3.12. Давайте скачаем и установим:
$ git clone https://github.com/void-deref/FunGP.git # скачиваем$ cd ./FunGP # переходим в папку проекта$ python –m venv .venv # создаем виртуальное окружение python$ source ./venv/bin/activate # (для Windows: .venv\Scripts\activate.bat) активируем виртуальное окружение$ pip install –e . # устанавливаем библиотеку со всеми зависимостями как локальный проект.
Теперь в корневой директории создайте папку «resources» и поместите в нее файл SM_applet.cap. Далее переходим в папку ./FunGP/tests/sm_applet и запускаем скрипт 01_install_applet:
Скрытый текст
Большой лог:
$ python 01_install_applet.pyCommand: Select ISD>> 00A40400 00<< 6112 [ Response bytes still available (ISO 7816) (18 bytes remaining)]Get response: 18 more bytes>> 00C00000 12<< 6F108408A000000151000000A5049F6501FF<< 9000 [ OK ]duration: 0.04Command: Initialize update>> 80500000 08 0C8E3E8AD844753A<< 611c [ Response bytes still available (ISO 7816) (28 bytes remaining)]Get response: 28 more bytes>> 00C00000 1C<< 00000000000000000000FF02004C09CC3FC6981D6ABFA5D3F15E4704<< 9000 [ OK ]duration: 0.04Key diversification data: 00000000000000000000KVN and SCP ID : FF02Key Sequence counter : 004CCard challenge : 09CC3FC6981Dcard cryptogram : 6ABFA5D3F15E4704host cryptogram : D82433D0883D9EA8Command: External authenticate>> 84820100 10 D82433D0883D9EA8C95CC3F6C0B8F85B<< 9000 [ OK ]duration: 0.04Command: INSTALL[for load]>> 84E60200 20 05A00000008300000EEF0CC602FFFFC702FFFFC802FFFF00E16DA4D84EE5012F<< 6101 [ Response bytes still available (ISO 7816) (1 bytes remaining)]Get response: 1 more bytes>> 00C00000 01<< 00<< 9000 [ OK ]duration: 0.04Command: LOAD>> 84E80000 FF C48206FA01000FDECAFFED010204000105A00000008302001F000F001F000F0029010A003103D300DA009100000269000C000500C104010004002904000107A0000000620001050107A0000000620102050107A0000000620101050107A000000062020103000F010BA0000000837365636D736701B506003100800000FF00010700000019001F0025002B003100AB00AF00800000FF00010000000182030900090701000001C78200000703D303003B80570094002501F90081027C003101F980810286002501107B001B7701107B001C7701107B001D7701107B00207701107B0023770422033119188B002703188B0027928E04002B7AEE1B818678D61E<< 6101 [ Response bytes still available (ISO 7816) (1 bytes remaining)]Get response: 1 more bytes>> 00C00000 01<< 00<< 9000 [ OK ]duration: 0.04# тут еще куча последовательностей команд LOAD# Последняя команда LOADCommand: LOAD>> 84E88007 45 0604030303070708310905070604080408090A04090E0705070E06051F060608081F0E0B0C0A0B0A090F1309090C160C250B0B090C1A0B080B13080B170C1C5B79AFBA2421<< 6101 [ Response bytes still available (ISO 7816) (1 bytes remaining)]Get response: 1 more bytes>> 00C00000 01<< 00<< 9000 [ OK ]duration: 0.08Command: INSTALL[for install and make selectable]>> 84E60C00 4E 05A0000000830BA0000000837365636D73670BA0000000837365636D7367010024C920000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1FEF000089C50B4656137FD8<< 6101 [ Response bytes still available (ISO 7816) (1 bytes remaining)]Get response: 1 more bytes>> 00C00000 01<< 00<< 9000 [ OK ]duration: 0.07***** CAP-file size ********** CAP-file parameters *****Package AID: A000000083Applet AID: A0000000837365636D7367Applet size: 1786 bytes.Reader: context has been released.
Если все прошло успешно, то давайте разберем, что такое интересное есть в этом скрипте. А если не совсем гладко — то тем более.
FunGP: 01_install_applet.py
Это вводный скрипт, цель которого максимально комфортно накатить апплет.
from fun_gp import Reader, SmartCard, SCP02, CCM, InstallParams, APPLET_PATHisd_keyset = ['404142434445464748494A4B4C4D4E4F', '404142434445464748494A4B4C4D4E4F', '404142434445464748494A4B4C4D4E4F']cap_path = APPLET_PATH / 'SM_applet.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.mutual_auth() install_params = InstallParams( app_params='000102030405060708090A0B0C0D0E0F 101112131415161718191A1B1C1D1E1F' ) isd.install_app_scp02(cap_path, install_params, 0x90, 0x00)
На что стоит обратить внимание:
-
with Reader() as reader– архитектура класса заточена именно под эту конструкцию, чтобы при аварийном завершении скрипта гарантированно высвободить системные ресурсы, выделенные под ридер. -
У класса
SmartCard()два последних параметра «по требованию», например, как в этом скрипте, ведь для установки апплета нам нужен экземпляр классаSCP02(Secure Channel Protocol 02, GlobalPlatform) для взаимодействия с доменом безопасности иCCM(Card Content Management, GlobalPlatform), формирующий все необходимые команды, а также разбирает cap-файл на составные компоненты. -
У метода
SmartCard.transmit()четыре параметра: целеваяAPDUкоманда,SW1, SW2и человеко-читаемое название, выводимое в консоль. Последние три также являются опциональными, при этом еслиSW1илиSW2заданы, значит мы ждем конкретно эти значения, иначе скрипт упадет с ошибкой. -
SmartCard.mutual_auth()является основным клиентом классаSCP02, он выполняет процедуру взаимной аутентификации между Хостом и картой в соответствии со стандартами ISO 7816 и GlobalPlatform. -
InstallParams()– это именно те данные, которые мы хотим видеть под тегом0xC9при установке апплета, именно эти данные попадут в конструктор нашего апплета. -
SmartCard.install_app_scp02()– занимается непосредственной установкой апплета на карту.
FunGP: 03_generate_shared.py
Итак, мы подошли к первому значимому скрипту, посредством которого попробуем сгенерировать секрет бобра и обменяться парой сообщений с апплетом. Начнем.
В методе main() есть следующее выражение:
resp, , = isd.transmit('8002 0000' + lv_hex(dh.pub_key), 0x90, 0x00).
Мы шлем апплету открытый ключ Хоста, а также ставим условие, что нас удовлетворяет только ответ 0x9000. Кстати, я вам говорил, что SmartCard.transmit() возвращает кортеж из трех элементов – R-APDU, SW1 и SW2, нет? Ну так вот, конкретно здесь нам нужен лишь resp с байтами открытого ключа апплета, которые передаются сюда:
shared_secret = dh.generate_shared_secret(resp)
Далее первые 16 байт списка “shared_secret” используем как сессионный ключ для симметричного шифрования на алгоритме AES128 с методом дополнения до кратности блока ISO 9797 M2 (0x80, 0x00 .. 0x00):
dh.init_aes_cipher(shared_secret[0:16])
И в конце вызываем две вспомогательные функции, осуществляющие операции по шифрованию и дешифровке:
def send_cipher_text(isd:SmartCard, dh:DiffieHellman): input_data = '0102030405060708090a0b0c0d0e0f' # encrypting plain text cipher_text = dh.aes_encrypt(input_data) # passing over cipher text resp, _,_ = isd.transmit('8004 0100' + lv_hex(cipher_text), 0x90, 0x00, cmd_name='Decrypt data') # plain_text = dh.aes_decrypt(resp) # conparison print(f'input data: {input_data}\nplain text: {bytes_to_hex(resp)}') assert input_data.lower() == bytes_to_hex(resp).lower(), f"Assertion failed: expected {input_data}, got: {bytes_to_hex(resp)}"def send_plain_text(isd:SmartCard, dh:DiffieHellman): # passing over plain text input_data = '0102030405060708090a0b0c0d0e0f' resp, _,_ = isd.transmit('8004 0200' + lv_hex(input_data), 0x90, 0x00, cmd_name='Encrypt data') # decrypting result plain_text = dh.aes_decrypt(resp) # conparison print(f'input data: {input_data}\nplain text: {bytes_to_hex(plain_text)}') assert input_data.lower() == bytes_to_hex(plain_text).lower(), f"Assertion failed: expected {input_data}, got: {bytes_to_hex(plain_text)}"
Все очень просто, давайте на примере функции send_cipher_text():
-
Метод
dh.aes_encrypt(input_data)вернет результат шифрования входных данных “0102030405060708090a0b0c0d0e0f”, которые мы пошлем карте; -
Карта в свою очередь вернет результат расшифровки данных из п.1;
-
Скрипт упадет если исходный текст не равен результату, возвращенному апплетом.
По аналогии работает метод send_plain_text().
FunGP: 04_secret_exchange.py
Этот скрипт имеет общий main() с предыдущим, а отличается лишь вспомогательными функциями последовательного считывания данных из массива ‘secret’ апплета, перезаписи его новыми и повторного считывания.
def fetch_secret(isd:SmartCard, dh:DiffieHellman): resp,_,_ = isd.transmit('8006 0000 20', 0x90, 0x00) plain_text = dh.aes_decrypt(resp) print(f'Card\'s secret: {bytes_to_hex(plain_text)}')def store_secret(isd:SmartCard, dh:DiffieHellman): cdata = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' cipher_text = dh.aes_encrypt(cdata) _,_,_ = isd.transmit('8008 0000' + lv_hex(cipher_text), 0x90, 0x00)
Обратите внимание, что в store_secret() мы игнорируем все элементы ответа апплета, т.к. sw1 и sw2 и так проверяются внутри метода SmartCard.transmit(), а resp нам не нужен т.к. команда ‘8008 0000‘ (STORE SECRET) не возвращает никаких данных вообще.
FunGP: тестирование SW1 SW2
Как отмечалось ранее, параметры sw1, sw2 метода SmarCard.transmit() обозначают ожидаемый результат выполнения команды. Для интереса попробуйте повторно вызвать скрипт 01_install_applet.py и он упадет со следующим логом:
Command: INSTALL[for load]>> 84E60200 20 05A00000008300000EEF0CC602FFFFC702FFFFC802FFFF0045A8710AEBD1B86F<< 6985 [ Conditions of use not satisfied ]duration: 0.04Reader: context has been released.Traceback (most recent call last): File "/home/telman/develop/FunGP/tests/sm_applet/01_install_applet.py", line 20, in <module> install_applet() File "/home/telman/develop/FunGP/tests/sm_applet/01_install_applet.py", line 18, in install_applet isd.install_app_scp02(cap_path, install_params, 0x90, 0x00) File "/home/telman/develop/FunGP/fun_gp/smart_card.py", line 52, in install_app_scp02 self.transmit(for_load, exp_sw1, exp_sw2, 'INSTALL[for load]', is_secured=is_secured) File "/home/telman/develop/FunGP/fun_gp/smart_card.py", line 14, in transmit return self._plain_apdu(cmd, exp_sw1, exp_sw2, cmd_name) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/telman/develop/FunGP/fun_gp/reader.py", line 115, in plain_apdu raise SWMismatchException(f"\n\nCard response error: {description} {attempts_left}"fun_gp.reader.SWMismatchException: Card response error: Conditions of use not satisfied expected: 9000 got: 6985
Согласно GlobalPlatform именно эта ошибка должна быть возвращена при попытке установить апплет с package AID идентичным тому, что уже присутствует на карте. Также можете прогнать 05_test_has_cdata.py, в котором команда обмена открытыми ключами претерпела существенное изменение:
card_pub_key,_,_ = isd.transmit('8002 0000', 0x67, 0x00, cmd_name='Public key exchange between Host and Card')
А именно, C-APDU лишен полезной нагрузки (открытого ключа Хоста). Тем не менее, скрипт завершится штатно, без гигантской портянки с трассировкой места падения:
Command: Select Secure Message applet>> 00A40400 0B A0000000837365636D7367<< 9000 [ OK ]duration: 0.09Command: Public key exchange between Host and Card>> 80020000 00<< 6700 [ Wrong length ]duration: 0.05Reader: context has been released.
А все потому, что в сигнатуре метода сказано, что мы ожидаем SW1=0x67 и SW2=0x00, что мы и получили.
На этом все, дорогие читатели, надеюсь, материал окажется полезным.
Всех благ.
ссылка на оригинал статьи https://habr.com/ru/articles/1061992/