В этой статье разговор пойдет о приложении на 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 # Ubuntu Buildfile: /home/telman/develop/SecMsgApplet/build.xml dist: [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.cap BUILD SUCCESSFUL Total time: 0 seconds
Как результат, в корневой директории появится файл «SM_applet.cap». Так, со сборкой разобрались, теперь давайте посмотрим, что мы собираем.
Апплет: метод install()
В обязанности JCp входят задачи по администрированию апплета на всех этапах его жизненного цикла - с момента установки и вплоть до удаления. Сущность, которая отвечает за эту часть работы именуется JCRE. Вот именно она и дергает метод install(), чтобы создать экземпляр класса.
public static void install(byte[] params, short off, byte len) { Main applet = new Main(params, off, len); applet.register(); }
Внутри этого метода создается экземпляр нашего апплета, в конструктор которого передается массив с пользовательскими данными.
public Main(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 length off += len; len = params[off++]; // Control info length off += 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 boolean has_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 short ciphering(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.py Command: 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.04 Command: 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.04 Key diversification data: 00000000000000000000 KVN and SCP ID : FF02 Key Sequence counter : 004C Card challenge : 09CC3FC6981D card cryptogram : 6ABFA5D3F15E4704 host cryptogram : D82433D0883D9EA8 Command: External authenticate >> 84820100 10 D82433D0883D9EA8C95CC3F6C0B8F85B << 9000 [ OK ] duration: 0.04 Command: 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.04 Command: 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 # Последняя команда LOAD Command: 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.08 Command: 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: A000000083 Applet AID: A0000000837365636D7367 Applet size: 1786 bytes. Reader: context has been released.
Если все прошло успешно, то давайте разберем, что такое интересное есть в этом скрипте. А если не совсем гладко - то тем более.
FunGP: 01_install_applet.py
Это вводный скрипт, цель которого максимально комфортно накатить апплет.
from fun_gp import Reader, SmartCard, SCP02, CCM, InstallParams, APPLET_PATH isd_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.04 Reader: 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.09 Command: Public key exchange between Host and Card >> 80020000 00 << 6700 [ Wrong length ] duration: 0.05 Reader: context has been released.
А все потому, что в сигнатуре метода сказано, что мы ожидаем SW1=0x67 и SW2=0x00, что мы и получили.
На этом все, дорогие читатели, надеюсь, материал окажется полезным.
Всех благ.
