Ассиметричная криптография в Java Card

от автора

В этой статье разговор пойдет о приложении на 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() делают одну и ту же работу и отличаются лишь полярностью:

  1. Проверка наличия защищенного канала;

  2. Длина данных строго 32 байта;

  3. Инициализация AES-шифратора;

  4. Целевая операция.

С апплетом, вроде, разобрались, двигаемся в сторону библиотеки тестирования.

библиотека 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)

На что стоит обратить внимание:

  1. with Reader() as reader – архитектура класса заточена именно под эту конструкцию, чтобы при аварийном завершении скрипта гарантированно высвободить системные ресурсы, выделенные под ридер.

  2. У класса SmartCard() два последних параметра «по требованию», например, как в этом скрипте, ведь для установки апплета нам нужен экземпляр класса SCP02 (Secure Channel Protocol 02, GlobalPlatform) для взаимодействия с доменом безопасности и CCM (Card Content Management, GlobalPlatform), формирующий все необходимые команды, а также разбирает cap-файл на составные компоненты.

  3. У метода SmartCard.transmit() четыре параметра: целевая APDU команда, SW1, SW2 и человеко-читаемое название, выводимое в консоль. Последние три также являются опциональными, при этом если SW1 или SW2 заданы, значит мы ждем конкретно эти значения, иначе скрипт упадет с ошибкой.

  4. SmartCard.mutual_auth() является основным клиентом класса SCP02, он выполняет процедуру взаимной аутентификации между Хостом и картой в соответствии со стандартами ISO 7816 и GlobalPlatform.

  5. InstallParams() – это именно те данные, которые мы хотим видеть под тегом 0xC9 при установке апплета, именно эти данные попадут в конструктор нашего апплета.

  6. 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():

  1. Метод dh.aes_encrypt(input_data) вернет результат шифрования входных данных “0102030405060708090a0b0c0d0e0f”, которые мы пошлем карте;

  2. Карта в свою очередь вернет результат расшифровки данных из п.1;

  3. Скрипт упадет если исходный текст не равен результату, возвращенному апплетом.

По аналогии работает метод 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/