Expertus metuit
Смарт-карты и программирование (python)
Опубликовано 2023-10-16 в 22:14

Эта статья является уже третьей в серии об использовании смарт-карт. Первая — Смарт-карты и программирование — была написана в 2017 году и все примеры в ней были на языках C и C++. Вторая — Смарт-карты и программирование (java) — в 2019 и в качестве базового использовался язык Java. И вот пришло время для очередной переработки, на этот раз всё на примере python3, код на котором получается компактным, выразительным и понятным. Кроме нового языка за эти годы накопились отзывы, замечания и предложения, появился опыт использования других устройств (криптографических USB-токенов, других типов бесконтактных карт), всё это отражено в тексте, который значительно отличается от предыдущих статей в структуре и формулировках.

Если вы только знакомитесь с этими технологиями, рекомендую читать именно эту статью, я постарался в ней учесть все недостатки прошлых, уточнить и прояснить терминологию, а также более детально раскрыть некоторые моменты. Ещё я добавил несколько новых разделов, которых не было в прошлых статьях: чипы Mifare Ultralight, NFC-метки и NDEF, прямое обращение к радиомодулю считывателя для выполнения произвольных команд NFC, банковские карты МИР, SIM-карты.

Мы все активно пользуемся смарт-картами, даже не зная этого, чиповые банковские карты, NFC-метки, бесконтактные пропуски-ключи, USB-ключ для электронной подписи, сим-карты в телефонах — это всё смарт-карты. В этом очень большом тексте я детально расскажу об использовании смарт-карт на уровне прикладного ПО. Здесь будет много теории, много ссылок на стандарты и спецификации, много кода. Я не ставил цель написать энциклопедию, для этого формата статьи не хватит, но дать обзорное представление о предметной области вполне можно. В конце есть список книг, в которых тема смарт-карт раскрыта детально и системно.

Я выбрал Python за его простоту, доступность, удобство использования и богатую библиотеку модулей. От вас не требуется никаких дополнительных знаний, кроме Python и операционной системы, на которой вы будете запускать код. Изначально всё программное окружение ориентировано на Unix-подобные системы — разнообразные варианты Linux и macOS, однако всё должно работать и в Windows (однако я не проверял). Все демонстрационные программы используют консоль (терминал), поэтому должны работать примерно одинаково везде, я не использую никаких GUI-библиотек, только командную строку.

В тексте статьи нет разделения на отдельный блок с теорией и примерами, вместо этого теория и примеры чередуются, поэтому вы увидите первый код практически в самом начале.

Практически все доступные сторонние материалы (стандарты, спецификации, книги) написаны по-английски, поэтому вам нужно его знать, если хотите самостоятельно изучить тему более глубоко.

Если у вас есть предложения по содержимому, можете писать прямо в комментариях или напрямую на мой email: [email protected]

Для полного знакомства вам понадобится дополнительное оборудование — считыватели (ридеры) для смарт-карт, криптографические токены, разнообразные смарт-карты, подробнее об этом я расскажу ниже.

Что такое смарт-карта

Изначально смарт-карта представляла собой пластиковую карточку формата ID-1 размером 85,60 × 53,98 мм со скруглёнными углами (стандартная банковская/кредитная карточка имеет такую же форму и размеры). В неё вмонтирован микрочип, коммуникационные контакты которого выведены на одну сторону в виде металлических контактных площадок стандартизированного размера.

ID-1 cards

Примеры смарт-карт: сим-карта в стандартной форме, клиентская карта системы НТВ-плюс, пустая неперсонифицрованная смарт-карта SLE5542.

Позднее появились смарт-карты в формате ID-000, они стали использоваться в мобильных телефонах и нам известны как SIM-карты. До сих пор операторы сотовой связи продают SIM-карты в формате ID-1, из которого ID-000 можно выломать по заранее оставленным надрезам и вставить в телефон.

Потом изобрели смарт-карты без внешней контактной площадки, в которых для питания применяется электромагнитная индукция и для коммуникации используется радиоканал. Также API для работы со смарт-картами используют некоторые USB-устройства, например, крипто-токены (Yubikey, eToken, Aladdin итп).

Общая для всех смарт-карт особенность: они не имеют собственного источника питания и полагаются на внешнее.

Основным преимуществом смарт-карты является физическая безопасность размещённых на ней данных. Микрочип очень маленький и всё «оборудование» микрокомпьютера размещено на нём, без вывода наружу внутренних контактов, к которым можно подключиться для перехвата. Также карты разрабатывались с учётом других возможных атак: сканирования рентгеновскими лучами, микрошлифования и так далее.

Примерный жизненный цикл смарт-карты (например, банковской карты с чипом) состоит из нескольких этапов. В нём участвуют производитель чипа, производитель смарт-карты, заказчик и клиент:

  1. Изготовление чипа/процессора. На этой стадии непосредственный изготовитель чипа после физического производства также записывает на него предоставленную производителем смарт-карт универсальную и одинаковую для всех карт партии «прошивку».
  2. Первичная инициализация смарт-карты. Партия чипов отправляется производителю карт, он модифицирует прошивку необходимым образом, например, прописывает внутрь каждой карты уникальный серийный номер, после чего специальным API-запросом отключает возможность менять его.
  3. Изготовление смарт-карты. Производитель вставляет чип в карты нужного формата и отправляет их заказчику, например, банку.
  4. Персонализация. Заказчик, используя методы прошитого ПО на карте, записывает на неё свои приложения, например, банковские; а также дополнительные данные, например, имя клиента, номер счёта и так далее. После чего специальным запросом финализирует карту, после чего ограничивается запись новых приложений, например.
  5. Выдача. Карта отдаётся клиенту и он её использует по назначению.
  6. Утилизация. Карта выбрасывается или уничтожается.

Главным стандартом для смарт-карт является ISO/IEC 7816, он охватывает все аспекты устройства, коммуникации и эксплуатации карт с контактной площадкой (контактных карт).

Для бесконтактных смарт-карт используется стандарт ISO/IEC 14443, он описывает детали, специфичные именно для устройств с радио-интерфейсом. Позднее он вошёл в более широкую группу, куда также входят стандарты для NFC.

Существует несколько других стандартов, о некоторых из них я расскажу позднее. Сразу отмечу, что я не буду рассказывать о физических и низкоуровневых деталях функционирования смарт-карт, нас интересуют только сравнительно высокоуровневые операции.

Необходимое оборудование

Статья полностью посвящена работе со смарт-картами на компьютере (десктопе или сервере). Для работы с бесконтактными картами (RFID/NFC) я использую популярное, недорогое и распространённое устройство-считыватель ACS ACR122U, оно подключается по USB, имеет драйвера для всех операционных систем и позволяет выполнять многие NFC-операции. Его легко купить как в России (например, на озоне он в марте 2023 года он стоил около 3000 рублей), так и заказать из Китая (например, через Aliexpress).

ACS ACR122U-A9

Считыватель бесконтактных карт ACS ACR122U-A9.

Если у вас другой считыватель для бесконтактных карт, некоторые примеры могут не заработать, поскольку работа с такими считывателями в нашем программном окружении происходит через трансляцию команд бесконтактных протоколов в команды PC/SC.

❈ ❈ ❈

Для использования смарт-карт с контактной площадкой (контактных смарт-карт) я купил другое устройство этого же производителя — ACS ACR38U, это тоже очень популярный и недорогой считыватель и тоже подключается через USB. Его тоже можно легко купить через ozon или aliexpress.

ACS ACR38U-I1

Считыватель контактных карт ACS ACR38U-I1.

Драйверы для этих устройств можно скачать с официального сайта производителя:

Впрочем, драйвера для обоих устройств одинаковые, достаточно скачать и установить любой из пакетов. В линуксе и макоси оба устройства уже работают «из коробки» без необходимости ставить дополнительные библиотеки.

Далее по тексту все устройства для подключения смарт-карт я буду называть считывателями, это уже устоявшийся в русском языке термин, практически дословный перевод англоязычного названия — reader. Иногда их ещё называют терминалами, однако в этой статье я буду пользоваться только словом считыватель, чтобы не возникало путаницы с терминалом-консолью операционной системы или терминалом как платёжным устройством.

Ещё нам понадобится для экспериментов несколько разных смарт-карт. Например:

  • банковские чиповые карты, включая карты с NFC (MasterCard PayPass, Visa PayWave);
  • стандартная белая пустая бесконтактная карта из типичного комплекта ACR122U на aliexpress («фабричная NFC-карта», Mifare Classic 1K, Mifare Classic 4K);
  • стандартная белая пустая контактная карта (например, SLE5542 или SLE5528);
  • Универсальная Электронная Карта (УЭК, хоть для чего-то пригодилась!);
  • московская транспортная карта Тройка;
  • новосибирская Единая Транспортная Карта;
  • или транспортная карта вашего города;
  • российский биометрический загранпаспорт (в нём есть NFC-чип);
  • USB-токены (например, yubikey или Rutoken);
  • NFC-метки;
  • CAM-карта спутникового телевидения (НТВ-плюс, Триколор);
  • SIM-карта любого оператора сотовой связи (чтобы её использовать в стандартном считывателе, вам понадобится вставить её обратно в прорезь исходной карты формата ID-1 и закрепить скотчем);
  • карта аутентификации для евро-тахографа;
  • смартфон с поддержкой NFC.

Существуют и другие бесконтактные карты в таком же исполнении ID-1, например, пропуски для офисных турникетов (HID, EM Marin, EM4100), однако они изготовлены по совершенно иному стандарту, работают на других частотах и в этой статье не рассматриваются. У меня есть обзорная статья в том числе и этого формата — Электронные ключи и карты для СКУД.

Для экспериментов с записью карт я рекомендую купить сразу набор из нескольких карт Mifare Classic 1K, Mifare Ultralight, NTAG 213, NTAG 215, они сравнительно недорогие и продаются на ozon или aliexpress.

❈ ❈ ❈

Также я настоятельно рекомендую установить на ваш android-смартфон приложение NFC Tools — это очень полезный инструмент для исследования NFC-устройств (карт, чипов, меток): чтения, записи, выполнения произвольных команд.

Программное обеспечение и окружение

Базовая операционная система для экспериментов — Mac OS X или Debian/Ubuntu, но подойдёт любой другой дистрибутив linux. Язык программирования — Python 3 (код написан и протестирован на Python версии 3.10). Все примеры кроссплатформенные и должны без модификаций запускаться на linux (debian/ubuntu) и Mac OS X. Работоспособность на Windows я не проверял, но должно работать и там тоже.

Debian/Ubuntu Linux

В этой статье активно используется консоль (она же shell-сессия, она же терминал) для выполнения команд, поэтому текст типа «выполните следующую команду» следует рассматривать читать как «выполните следующую команду в консоли/терминале». В листингах для обозначения именно команды используется символ процента (%) в начале каждой команды, он символизирует командную строку и его не нужно копировать в терминал при выполнении команд.

Иногда в листингах вместе с командой приводится ожидаемый результат её выполнения, он выводится шрифтом серого цвета.

Все нужные для работы пакеты ставятся из стандартного репозитория:

% sudo apt install pcscd python3 python3-pyscard

После установки должен автоматически запуститься сервис pcscd, однако на некоторых системах этого не происходит, поэтому на всякий случай выполните ещё две команды:

% sudo systemctl enable pcscd
% sudo systemctl enable pcscd.socket

Mac OS X

Весь код проверен на последней версии macOS 13.2 Ventura, используется штатный интерпретатор Python, однако для работы нам нужны нестандартные библиотеки, которые мы поставим в отдельное виртуальное окружение, чтобы не мешать другим python-программам.

Но для начала необходимо установить инструменты XCode для командной строки, это делается такой командой:

% xcode-select --install

Далее нужно проинициализировать виртуальное окружение, эту операцию необходимо проделать только один раз. Запускаем следующие команды:

% python3 -m venv ~/.venv-pyscard
% source ~/.venv-pyscard/bin/activate
% pip install wheel swig
% pip install pyscard ndeflib
% deactivate

Этими командами мы инициализируем виртуальное окружение в каталоге ~/.venv-pyscard, активируем для текущей консольной сессии командой source ~/.venv-pyscard/bin/activate и устанавливаем необходимые для работы пакеты. В конце деактивируем окружение командой deactivate.

Важное замечание

Я предполагаю, что вы уже знакомы с модулем venv и принципами работы виртуального окружения python, поэтому об этом не буду рассказывать подробно. Только отмечу, что для запуска примеров нужно каждый раз активировать в консольной сессии это виртуальное окружение командой source ~/.venv-pyscard/bin/activate. Пожалуйста, не забывайте об этом, в дальнейшем я предполагаю, что все команды запускаются именно в этом активированном виртуальном окружении.

Исходный код примеров

Исходный код всех примеров лежит в моём репозитории на github, который был создан специально для этой статьи: https://github.com/sigsergv/pcsc-tutorial-python. Я рекомендую такой режим работы с текстом:

  • скачайте репозиторий с github:
  • читайте текст;
  • изучайте примеры кода;
  • запускайте программы из соответствующего каталога (example-01, example-02 и так далее);
  • модифицируйте и распространяйте программы где угодно при условии сохранения ссылки на автора или репозиторий на гитхабе (весь код опубликован под лицензией New BSD License).

Даже если у вас нет аппаратного считывателя, я рекомендую всё равно читать разделы с соответствующими примерами.

Код написан исключительно для образовательных и демонстрационных целей и не соответствует критериям промышленного качества. Например, там очень мало проверок входных данных и аргументов программ.

Репозиторий разбит на каталоги с именами example-01, example-02 и так далее, в каждом из них находится как минимум одна программа в виде файла без расширения, например, list-readers. В каждом разделе с примером обязательно приводится ссылка на полный исходный код программы, поскольку тексте для краткости обычно будет написан только важный существенный фрагмент кода.

Соглашения о представлении данных

Мы будем много работать с бинарными (двоичными) данными, для представления которых будем использовать шестнадцатеричный (он же hex) формат по умолчанию. Сегмент данных будем записывать в виде списка шестнадцатеричных значений-октетов (длиной 8 бит), дополненных нулём при необходимости и опционально разделённых пробелами. Октеты также будем называть байтами.

Когда будет идти речь об отдельном значении-октете, его будем записывать в классической нотации с префиксом 0x, например, 0x9A или 0x08. Если никакого суффикса нет, то подразумевается десятичное представление (например, 0x9A равно 154).

⭐ Во многих спецификациях и стандартах используется нотация с суффиксом h для обозначения шестнадцатеричных значений и суффиксом d для обозначения десятичных. В такой нотации значения записываются как 9Ah и 08h. Такую нотацию я в моей статье использовать не буду.

В коде мы будем активно пользоваться строковым представлением сегментов данных для читабельности, записанных в hex-формате, например, FF 00 00 00 02. Представленные в таком виде сегменты для реального использования конвертируются в списки байтов, это именно традиционные списки, состоящие из байтов, а не встроенные типы bytes или bytearray.

Отладка/логирование в Mac OS X

В OS X 10.10 Yosemite появился штатный инструмент для мониторинга активности считывателей смарт-/nfc-карт. Однако в разных версиях он активируется и используется по-разному.

Для версии macOS 10.12 Sierra и вплоть до последней на момент написания статьи macOS 13.2 Ventura последовательность включения логирования такая:

  • отключаем считыватель от USB-порта;
  • включаем логирование командой:

    % sudo defaults write /Library/Preferences/com.apple.security.smartcard Logging -bool true
    
  • снова подключаем считыватель;

  • смотрим логи в shell-сессии командой:

    % log stream --style compact --predicate 'process == "com.apple.ifdreader"'
    

    или для совсем компактного вывода:

    % log stream --style compact --predicate 'process == "com.apple.ifdreader"'|sed -E -e 's/ com.apple.+APDULog]//'
    

Режим логирования выключается автоматически при отключении устройства от USB-порта. Вручную логирование отключается командой:

% sudo defaults write /Library/Preferences/com.apple.security.smartcard Logging -bool true

Очень удобно открыть лог в отдельном окне shell-сессии и оставить там работать.

❈ ❈ ❈

В OS X 10.10 Yosemite и OS X 10.11 El Capitan последовательность другая.

Включается логирование вот такой командой перед подключением считывателя к USB-порту:

% sudo defaults write /Library/Preferences/com.apple.security.smartcard Logging -bool true

После этого можно смотреть логи из shell-сессии:

% syslog -w -k Sender com.apple.ifdreader
Mar 10 21:23:03 mba com.apple.ifdreader[64546] <Notice>: card in
Mar 10 21:23:03 mba com.apple.ifdreader[64546] <Notice>: ATR:3b 8f 80 01 80 4f 0c a0 00 00 03 06 03 00 01 00 00 00 00 6a
Mar 10 21:23:04 mba com.apple.ifdreader[64546] <Notice>: card out

Режим логирования выключается автоматически при отключении устройства от USB-порта. Вручную логирование отключается так:

% sudo defaults write /Library/Preferences/com.apple.security.smartcard Logging -bool false

Отладка/логирование в Linux

Сначала останавливаем демон pcscd, а затем запускаем его в отдельном терминале в режиме показа отправляемых команд:

% sudo service pcscd stop
% sudo pcscd --apdu --foreground
00000000 APDU: FF CA 00 00 0A 
00000591 SW: 9C 0C AA 99 0A 4F 0C A0 00 00 90 00 
...

Есть метод и без остановки демона, через программу pcsc-spy (она входит в состав пакета libpcsclite-dev), но он не всегда срабатывает. Сначала вы в одной терминальной сессии определяете переменную окружения и дальше в этой же сессии запускаете программы. Вот как это выглядит в Debian/Ubuntu на архитектуре amd64:

% export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libpcsclite.so

Затем в другом терминале запускаете pcsc-spy -n > pcsc-dump.log, а потом снова в первом терминале запускаете нужную программу, когда она отработает вы получите в файле pcsc-dump.log полный лог взаимодействия с библиотекой.

Теория: программные библиотеки

Для разработчика фактически доступен только один высокоуровневый интерфейс для работы со смарт-картами и NFC-устройствами, он реализован в виде нескольких системных сервисов, а его интерфейсная часть — фреймворк PC/SC. PC/SC расшифровывается как Personal Computer/Smart Card и был портирован из Windows в Linux/macOS с практически идентичным API.

PC/SC является спецификацией, предложенной PC/SC Workgroup, именно работе с ней (и только с ней!) посвящена эта статья. Другие низкоуровневые команды, протоколы и спецификации не рассматриваются. Работа с NFC также происходит исключительно через прослойку PC/SC.

PC/SC очень упрощает жизнь разработчика именно за счёт унификации, ему не нужно изучать низкоуровневые команды драйвера каждого устройства.

❈ ❈ ❈

В Python мы будем пользоваться модулем pyscard, который предоставляет высокоуровневый интерфейс к системной библиотеке PC/SC. Этот модуль скрывает большое количество многословного кода, который вы могли видеть в статье об использовании смарт-карт на языке C/C++. Основным информационным блоком данных в pyscard является список байтов, это именно список, состоящий из байтов. Не строка, не байтовая строка, не байтовый массив. Для преобразования разных типов данных в списки байтов и обратно библиотека предоставляет специальные функции.

❈ ❈ ❈

В статье много ссылок на функции, классы и модули, они выглядят как значок 📚 и ставятся за элементом, к которому относятся. Клик по такой ссылке откроет соответствующий раздел документации по API в новом табе.

example-01: получение списка считывателей

Исходный код программы: example-01/list-readers

Цели этого примера:

  • дать общее представление, как выглядит работа со считывателем смарт-карт в программном коде;
  • на очень простом и компактном примере получить осязаемый результат;
  • о терминах и теории поговорим дальше, сначала — первая работающая программа.

Что конкретно нам нужно сделать:

  • подключиться к системной библиотеке PC/SC через модуль pyscard;
  • получить список доступных считывателей;
  • напечатать его на экране.

Вот весь код программы:

#!/usr/bin/env python3

from smartcard.System import readers

def main() -> int:
    print('List of available readers:')
    for i,r in enumerate(readers()):
        print('{0}: {1}'.format(i, str(r)))
    return 0

if __name__ == '__main__':
    main()

Для получения списка считывателей мы используем функцию readers() 📚 из модуля smartcard.System, поэтому её нужно явным образом импортировать, это делается в строке:

from smartcard.System import readers

При вызове эта функция возвращает список доступных считывателей вместе с индексом каждого считывателя в списке. Каждый элемент списка является объектом класса smartcard.pcsc.PCSCReader.PCSCReader 📚, который представляет конкретный считыватель. Чтобы получить его имя, мы используем стандартный для python способ — вызов функции str() на объекте.

⭐ На самом деле каждый из возвращаемых функцией smartcard.System.readers() элементов является объектом класса, который реализует базовый абстрактный класс smartcard.reader.Reader.Reader.

Если мы подключим к компьютера оба упомянутых ранее считывателя ACS ACR122U и ACS ACR38U, то результат запуска программы будет такой:

% python3 example-01/list-readers
List of available readers:
0: ACS ACR 38U-CCID
1: ACS ACR122U PICC Interface

example-02: чтение карты со считывателя

Исходный код программы: example-02/reader-read-card

Цели этого примера:

  • подключиться к конкретному считывателю;
  • если в нём нет карты, ждать, пока она там не появится;
  • прочитать что-нибудь с карты.

Для простоты будем считать, что у нас подключен только один считыватель, например, ACS ACR38U для контактных карт, а в этот считыватель пустой, без вставленной карты. И для примера возьмите любую свою банковскую карту с контактной площадкой.

Вот весь код программы:

#!/usr/bin/env python3

from smartcard.System import readers
from smartcard.CardRequest import CardRequest
from smartcard.util import toHexString

def main() -> int:
    reader = readers()[0]
    print('Connected reader: {0}'.format(reader))
    cardrequest = CardRequest(timeout=None, readers=[reader])
    print('Waiting for the card...')
    cardservice = cardrequest.waitforcard()
    cardservice.connection.connect()
    print('Inserted card with ATR: {0}'.format( toHexString(cardservice.connection.getATR())) )
    return 0

if __name__ == '__main__':
    main()

Здесь тоже всё просто. Сначала мы в переменную reader получаем объект, представляющий первый считыватель из доступных:

reader = readers()[0]

Поскольку в считывателе нет карты, нам необходимо дождаться её появления, это делается путём создания объекта класса smartcard.CardRequest.CardRequest📚 и последующего вызова метода waitforcard(). В конструкторе этого класса можно задать различные критерии, по которым мы ждём карту. В нашем случае мы указываем полученный ранее считыватель, а также определяем таймаут, в течение которого мы будем ждать, значение timeout=None означает, что ждать бесконечно.

cardrequest = CardRequest(timeout=None, readers=[reader])
print('Waiting for the card...')
cardservice = cardrequest.waitforcard()
cardservice.connection.connect()

Когда в указанный считыватель будет вставлена карта, выполнение метода waitforcard() прервётся и нам вернётся объект cardservice класса smartcard.CardService.CardService (или будет выброшено исключение). В поле объекта connection находится объект, представляющий установленное соединение до вставленной карты. Единственное, что мы гарантированно можем прочитать с карты — это её ATR, в следующем теоретическом разделе будет объяснение, что это такое, но сейчас достаточно только знать, что это небольшой набор байтов. Просто прочитаем его методом getATR() и напечатаем на экране, используя функцию toHexString()📚, которая печатает список байтов в человекочитаемом виде.

print('Inserted card with ATR: {0}'.format( toHexString(cardservice.connection.getATR())) )

Результат запуска программы такой:

% python3 example-02/reader-read-card
Connected reader: ACS ACR 38U-CCID
Waiting for the card...
Inserted card with ATR: 3B FF 97 00 00 80 31 FE 45 00 31 C1 73 C8 21 10 64 47 4D 30 30 00 90 00 E6

⭐ В вызове cardservice.connection.connect() можно также передать конкретный транспортный протокол, который мы хотим использовать для соединения (T=0 или T=1), однако мы этот аргумент пропускаем, полагаясь на автоматический выбор протокола. В дальнейшем у меня будут примеры для разных протоколов и особенностей запросов в каждом из них.

Мы пока ничего ничего не знаем о данных или функциях карты, однако у них у всех есть общее свойство — ATR (Answer-To-Reset) — это короткий (не более 33) массив байтов, который карта обязана передать считывателю при подключении. Если карта этого не делает за определённое время, считается, что она функционирует некорректно. В ATR содержится базовая информация о карте и технических параметрах соединения, но его формат довольно сложный и зависит от производителя чипа, установленных на карте «приложений» и так далее. ATR сохраняется в считывателе всё время, пока карта подключена.

Структура ATR определена в разделе 8.2 стандарта ISO/IEC 7816-3, а я детально расскажу об этом позднее, пока ограничимся только его байтовым представлением.

Важные моменты из этого примера

  • Нужно явным образом дожидаться готовности считывателя и появления в нём карты.
  • После подключения к карте все операции с ней выполняются через объект класса javax.smartcardio.Card.
  • После окончания работы с картой нужно обязательно отключиться методом disconnect(), если этого не сделать, считыватель может «зависнуть» и потребуется его физическое переподключение.
  • Карта при подключении к считывателю обязана сразу же передать свой ATR.
  • ATR не является идентификатором карты и не содержит идентификатор карты. Все карты одного типа могут иметь одинаковый ATR.
  • В интернете есть вебсервис, которым можно парсить ATR: https://smartcard-atr.apdu.fr/
  • Поэкспериментируйте с ATR разных карт или NFC-меток.
  • Вы можете вместо карты поднести к считывателю, например, смартфон с включённым NFC, тот его распознает и программа покажет ATR.
  • У бесконтактной карты (NFC) нет ATR, так как там иная процедура инициализации, однако считыватель для совместимости генерирует ATR сам и отдаёт его программе.

Теория: архитектура PC/SC

Для нас главным стандартом является официальная спецификация PC/SC Workgroup, её цель — описать единый унифицированный интерфейс для работы с определёнными классами считывателей карт. Спецификации PC/SC доступны для свободного скачивания на сайте организации: https://pcscworkgroup.com/specifications/, по тексту статьи я буду давать прямые ссылки на нужные стандарты.

На март 2023 года последней версией спецификаций является 2.01.14, выпущенная в 2013 году. Она состоит из десяти частей:

Спецификации PC/SC Workgroup основаны на следующих международных стандартах:

  • ISO/IEC 7816 — семейство стандартов, определяющих все аспекты работы со смарткартами с контактной площадкой;
  • ISO/IEC 14443 — семейство стандартов, определяющих все аспекты работы с бесконтактными пассивными устройствами ближнего радиуса действия (до 10 см) по радиоканалу на частоте 13,56 МГц;
  • ISO/IEC 15693 — семейство стандартов, определяющих все аспекты работы с бесконтактными пассивными устройствами дальнего радиуса действия (от 10 см до 1 м) по радиоканалу на частоте 13,56 МГц;
  • EMV — семейство стандартов для платёжных карт.

Некоторые из указанных стандартов доступны для свободного скачивания, для них выше даны ссылки.

Некоторые стандарты имеют официальные российские переводы и доступны для свободного (или относительно свободного) скачивания, ссылки на них в конце статьи в разделе Ссылки.

Термины, сокращения и типы данных

Сначала несколько терминов и сокращений, которые активно используются в стандартах (и дальше у меня в тексте).

ICC
Integrated Circuit Card, Карта на интегральной схеме, «смарт-карта» — пластиковая карта со встроенным чипом и контактной площадкой.
PICC
Proximity Integrated Circuit Card, Бесконтактная карта на интегральной схеме (название по ГОСТ Р ИСО/МЭК 14443-1) — пластиковая карта со встроенным чипом и радиоинтерфейсом. В ГОСТ 14443-1 написано следующее: Бесконтактная карта на интегральной схеме или другой объект, обмен данными с которой(ым) осуществляется посредством индуктивной связи в непосредственной близости от терминального оборудования.
VICC
Vicinity Integrated Circuit Card, Бесконтактная карта на интегральной схеме дальнего действия (на расстояниях около 1 м.) Я это сокращение тут упомянул только для общего развития, об этом протоколе я писать не буду.
IFD
Interface Device, Интерфейс/устройство сопряжения (название по ГОСТ Р ИСО/МЭК 7816-7), устройство считывания карт, считыватель, терминальное оборудование.
PCD
Proximity Coupling Device, IFD для PICC, считыватель NFC, NFC-сканер.
Application
Структуры, элементы данных и программные модули (на ICC/PICC), необходимые для выполнения определенных функций. Примеры приложений: банковское (Visa/MasterCard), телефонное (SIM для GSM-сети).
APDU
application protocol data unit, блок данных прикладного протокола: основной тип данных, который используется для связи ICC/PICC и IFD/PCD. APDU представляет собой байтовый массив с определённой логической структурой.
TPDU
transmission protocol data unit, блок данных транспортного протокола, именно TPDU используется при общении IFD (считывателя) с ICC (картой).

❈ ❈ ❈

Базовой минимальной единицей обмена данным в PC/SC является байт (byte), то есть целое число из восьми битов. В некоторых стандартах вместо слова байт используют термин октет (octet), чтобы подчеркнуть структуру значения, поскольку в теории длина байта может быть своей на оборудовании различных архитектур. На практике же мы всегда подразумеваем, что длина байта восемь бит, и дальше будем пользоваться именно этим термином.

Байт может иметь значение от 0 до 255 включительно в десятичном представлении, однако для нас удобнее использовать шестнадцатеричное (hex), причём всегда двухсимвольное, то есть байт может принимать значение от 0x00 до 0xFF включительно. Иногда ещё указывается, в каком виде кодируется число — little endian / big endian. В нашей предметной области (как и вообще в сетях) принят по умолчанию вариант big endian. То есть в памяти (и при передаче) первым идёт старший байт и данные передаются/сохраняются в той же последовательности, в какой они отображаются на экране.

В некоторых ситуациях удобно разделять байт на компоненты. Байт 0x9A состоит из двух полубайтов (по-английски nibble): 9 — первый/старший и A — второй/младший полубайт. Запись 9X означает: «любой байт, в котором старший полубайт равен 9».

Байты группируются в байтовые массивы, именно в такой последовательности байты хранятся в памяти и передаются по каналам связи. В стандартах и документации байтовые массивы обычно представляются строками из hex-литералов, разделёнными пробелами для читаемости: 00 90 FA 91 3C, а пробелы могут иногда опускаться: 0090FA913C.

Биты, из которых состоит байт, принято нумеровать справа налево в бинарной записи. Например, байт 0xC5 в битовом представлении выглядит так: 1 1 0 0 0 1 0 1. А биты нумеруются так:

b8b7b6b5b4b3b2b1
11000101

При этом бит b8 принятно называть старшим битом (higher bit, most significant bit/msb), а b1младшим (lower, least significant/lsb). Также выражение типа «три старших бита» означает «биты b8, b7, b6». Неформально принято сокращения в нижнем регистре относить к битам, а в верхнем — к байтам, то есть MSB означает most significant byte, а msbmost significant bit.

Иерархичная модель

Смарт-карта фактически является миниатюрным компьютером с микропроцессором, памятью и операционной системой. Внешние системы не имеют прямого доступа к его данным и должны пользоваться предоставленным интерфейсом (контактная площадка или радиоинтерфейс). У смарт-карт нет собственного питания и они получают его от считывателя через контакты (ICC) или индукционную катушку (PICC).

Архитектура PC/SC иерархичная.

Архитектура PC/SC

На самом нижнем уровне находятся физические считыватели (оборудование для чтения карт) — IFD/PCD и собственно карты — ICC/PICC. Между картами и считывателями используется электрический сигнальный протокол, он предельно низкоуровневый и оперирует электрическими характеристиками (частота, напряжение и так далее). У каждого IFD может быть свой способ подключения к компьютеру (USB, COM, PS/2, Firewire и т.д.), свой собственный протокол для работы через порт подключения.

Уровнем выше располагаются IFD Handler, по сути это нечто вроде драйверов, которые преобразуют специфичный сигнальный протокол каждого IFD в унифицированный, который используется уровнем выше.

На следующем уровне — ICC Resource Manager, менеджер ресурсов — это ключевой компонент всей инфраструктуры, он должен быть единственным в ОС и обычно уже предоставляется операционной системой. ICC Resource Manager управляет доступом до всех ICC и IFD, отслеживает появление в системе новых считывателей, подключение и отключение ICC/PICC. Также задачей менеджера является разделение доступа до IFD и транзакции. Именно через него должны проходить все коммуникации между прикладным/системным ПО и считывателями. Никакое другое ПО не должно иметь самостоятельного доступа до непосредственно IFD Handler. Однако, считыватель вполне может быть доступен, например, как USB-устройство, то есть какой-то другой драйвер может работать параллельно с IFD handler, минуя всю подсистему PC/SC.

В Linux роль ICC Resource Manager выполняет демон pcscd из пакета pcsc-lite, который обычно ставится отдельно из стандартного репозитория. В macOS и Windows менеджер является стандартной частью операционной системы.

Выше ICC Resource Manager по иерархии находятся все остальные компоненты (сервис-провайдеры) и программы операционной системы. Они могут работать как напрямую через менеджер, так и через друг-друга. На картинке выше нарисовано несколько таких сервис-провайдеров. Более подробно о них написано во вводной части к спецификации PC/SC (раздел 2.1.5). Один из самых распространённых сервис-провайдеров — крипто-провайдер, который предоставляет для прикладных приложений унифицированный интерфейс взаимодействия с криптографическими приложениями на смарт-картах или криптографических USB-токенах.

На последнем уровне находятся прикладные приложения. Они могут работать как с промежуточными сервис-провайдерами, так и напрямую с ICC Resource Manager. Естественно, приложение должно уметь работать с интерфейсом сервис-провайдера или с низкоуровневым интерфейсом менеджера.

Я лишь коротко рассказал об основных компонентах в архитектуре PC/SC, детально они описаны в официальной спецификации. Дальше в тексте я буду иногда использовать только что описанные термины и сокращения, например, вместо «считыватель» я буду писать «IFD», вместо «смарт-карта» — «ICC», а вместо «бесконтактная-карта» — «PICC».

Схема работы IFD и ICC

Рассмотрим сначала диаграмму состояний IFD и ICC (напомню, что ICC — это чиповая карта с контактной площадкой, в этом разделе рассматриваются только они, поскольку для бесконтактных карт механизм PC/SC эмулирует часть данных для единообразного представления, но об этом позднее).

IFD and ICC states

Концептуально действительно всё просто: при подключении карты (подаче питания) отправляется сигнал RESET. Это делается через отдельную выделенную контактную площадку RST. Чип на карте понимает, что пришло питание и запущен процесс инициализации, значит нужно отправить назад специальный байтовый массив ATR (расшифровывается как Answer-to-Reset). Карта должна это сделать очень быстро, иначе IFD посчитает её неисправной и прекратит взаимодействие.

⭐ О структуре ATR я рассказываю подробно дальше в этой статье. Однако сейчас отмечу два важных момента:

  • ATR генерируется микропроцессором контактной чиповой карты.
  • В ATR содержится необходимая для дальнейшей работы информация: протоколы, код производителя, параметры протоколов и так далее. ATR не является идентификатором карты! Главная его цель — корректная инициализация оборудования для дальнейшей работы.

Когда IFD получает ATR, он его разбирает, извлекает нужные для корректного соединения параметры и переключается в готовый для дальнейшего взаимодействия режим. В этом режиме коммуникации между IFD и ICC происходят путём отправки и получения байтовых массивов, называемых TPDUTransport Protocol Data Unit. А между ICC и прикладными программами общение происходит посредством APDUApplication Protocol Data Unit — это тоже байтовые массивы. Отправка APDU и обработка ответов — это главное, чем занимается программа, работающая с PC/SC. IFD (считыватель) преобразует APDU в TPDU, отправляет TPDU в карту, получает ответ также в виде TPDU и преобразует его в APDU.

Поскольку нас интересует прежде всего прикладной уровень, мы никогда не будем сталкиваться непосредственно с TPDU, а будем работать только с APDU.

Схема работы PCD и PICC

⭐ Напомню, что для бесконтактных карт (PICC) считыватель (IFD) называется PCD — Proximity Coupling Device.

Существует два типа PICC, отличающихся по сигнальному интерфейсу, они обозначаются как Type A и Type B. Оба типа определены в разделе 7 стандарта ISO/IEC 14443-2, они используют одну и ту же частоту — 13,56 МГц, но у них разные низкоуровневые и радиочастотные характеристики, а также разные схемы инициализации.

Процесс инициализации бесконтактной карты устроен существенно сложнее, чем для контактной. Бо́льшую его часть занимает так называемый цикл антиколлизии. Коллизия возникает, когда в электромагнитное поле PCD одновременно попадает более одной карты и PCD должен эти карты отличить друг от друга. Алгоритм этого процесса весьма сложный и занимает несколько десятков страниц описания в стандартах ISO/IEC 14443-2 и ISO/IEC 14443-3, поэтому я его здесь приводить не буду, да нам он особо и не понадобится — этим полностью занимается считыватель и драйвер.

Также у PICC в принципе отсутствует ATR, вместо него PCD получает от карты другие данные, на основе которых устанавливает соединение. Однако для совместимости с ISO/IEC 7816 (который, напомню, лежит в основе PC/SC) считыватель (PCD) сам вместо карты формирует ATR на основе данных из процедуры инициализации. Этот процесс описан в разделе 3.1.3.2.3 спецификации PC/SC part 3.

В любом случае, для клиента PC/SC процесс начала работы с PICC практически ничем не отличается от ICC и логически выглядит примерно так же, как на иллюстрации из предыдущего раздела. Однако нужно учитывать, что работающий через PC/SC считыватель должен понимать конкретные типы ISO-14443-совместимых устройств (PICC) и уметь преобразовывать прикладные APDU в соответствующие радиосигнальные команды PICC.

⭐ Разные производители считывателей могут одни и те же радиочастотные команды PICC преобразовывать в различные команды APDU. В этой статье все примеры написаны для ACS ACR122U и если попытаться использовать считыватель другого производителя, написанные программы могут не сработать.

Протоколы обмена данными

Физические аспекты протокола для общения с чиповой картой (с контактной площадкой) определены в стандарте ISO/IEC 7816-3 (ГОСТ Р ИСО:МЭК 7816-3, это нас не интересует, поскольку за это отвечают компоненты PC/SC. Аналогично с бесконтактными картами, физические протоколы для которых определены в ISO/IEC 14443.

Перед тем как начать обмен данными приложение должно указать протокол передачи, по которому будет идти работа. В PC/SC эти протоколы взяты из стандарта ISO/IEC 7816, их принято обозначать как T=0 и T=1:

  • T=0 — символьно-ориентированный протокол;
  • T=1 — блочно-ориентированный протокол.

И T=0, и T=1 являются асинхронными и полудуплексными, то есть передача данных в каждый момент может идти только в одну сторону: отправили команду → получили ответ. Символьно- и блочно-ориентированность означают, какого рода/размера минимальные блоки данных передаются, и хотя это технические детали реализации на низком уровне, выбор протокола определяет, какие APDU допускается конструировать и как именно получать ответ на запрос. В обоих протоколах имеется контроль целостности, в T=0 он работает побайтов, а в T=1 поблочно. Кроме того, для T=0 возможна ситуация, когда результат выполнения команды содержит слишком много данных, поэтому возвращается статусное слово 61 XX, где XX — количество байтов, которые нужно прочитать через специальную команду GET RESPONSE, так продолжается до тех пор пока все данные не будут переданы. Этот момент будет подробно рассмотрен в одном из примеров ниже.

Контактные карты (ICC) могут поддерживать как оба протокола, так и какой-то один. Для бесконтактных карт (PICC) в стандарте ISO/IEC 14443-4 допускается только протокол T=1, в некоторых источниках он также обозначается как T=CL.

Структура пары команда-ответ

⭐ «Драйверы» из PS/SC берут на себя всю низкоуровневую работу с протоколами, клиентам остаётся только сравнительно высокоуровневая часть: отправка и получение байтов стандартизированным образом.

Пара команда-ответ состоит из двух идущих подряд сообщений: APDU команды (Command APDU, C-APDU, от прикладной программы через считыватель к карте) и немедленно следующего за ним APDU ответа (Response APDU, R-APDU, от карты через считыватель к прикладной программе). Обычно когда пишут APDU, имеют в виду Command APDU, я тоже буду придерживаться такого правила в следующих разделах.

Каждый C-APDU представляет собой последовательность байтов со следующей структурой (Таблица из стандарта ISO/IEC 7816-4-2020):

Command-Response

APDU любой команды всегда начинается с заголовка фиксированной длины 4 байта, они обозначаются в порядке следования: CLA, INS, P1, P2.

CLA
байт класса, он определяет класс команды, если бит b8 установлен в 0, то это межотраслевой класс, если установлен в 1, то проприетарный класс, значение 0xFF стандартом ISO 7816 запрещено для использования при реальной передаче данных, однако оно используется в PC/SC для специальных команд для считывателя. Для команд межотраслевого класса структура байта CLA описана детально в стандарте ISO/IEC 7816-4, но я её здесь описывать не буду.
Примеры. CLA=A0 используется для SIM-карт, а CLA=8X — для платёжных/банковских карт.
INS
байт инструкции или командный байт, он определяет собственно команду, которую нужно выполнить. В отрасли принято использовать одинаковые коды инструкций для близких по смыслу операций. Стандартные межотраслевые команды приведены в ISO/IEC 7816-4, а в конкретных отраслях они используются в собственных проприетарных классах CLA.
P1
байт первого параметра, конкретное значение зависит от выполняемой команды
P2
байт второго параметра, конкретное значение зависит от выполняемой команды

❈ ❈ ❈

Следующие байты определяют дополнительные данные, передающиеся с командой и максимальную длину ожидаемого ответа.

Lc
Поле Lc кодирует значение Nc — количество байтов в поле данных команды. Здесь может находиться 0, 1 или 3 байта. Отсутствие этого поля означает, что Nc равно нулю и тогда следующий блок отсутствует. Если длина поля Lc 1 байт, то в нём должно содержаться значение Nc от 1 до 255 включительно. Если длина поля Lc 3 байта (расширенное поле Lc), то первый байт равен 0x00, а два последующих определяют значение Nc от от 1 до 65535 включительно. В поле Lc не может содержаться значение 0x00.
данные команды
Длина этого блока байтов должна быть равна значению Nc, которое кодируется предыдущим полем.
Le
Поле Le кодирует значение Ne — максимальное количество байтов, которые мы хотим получить в ответе. Если поле Le отсутствует, то Ne равно нулю, если Le=00, то Ne равно 256. Расширенное поле Le может состоять из:
  • если поле Lc отсутствует, трёх байтов 00 XX YY, тогда байты XX YY кодируют значение Ne от 1 до 65535, байты 00 00 кодируют значение Ne 65536
  • если поле Lc присутствует, двух байтов XX YY, которые кодируют значение Ne от 1 до 65535, байты 00 00 кодируют значение Ne 65536

⭐ Поля Lc и Le могут быть короткими или расширенными только одновременно, комбинировать разные типы нельзя. Возможность использования расширенных полей Lc и Le определяется в значении ATR; если это явно не разрешено, то можно использовать только короткие поля.

❈ ❈ ❈

После отправки C-APDU, ответ возвращается в виде R-APDU. Если команда не возвращает полезных данных, R-APDU имеет следующий вид:

SW1SW2

Если возвращаются данные, то такой:

│ │ │ RESPONSE DATA │ │ │ SW1SW2

R-APDU состоит как минимум из двух обязательных статусных байтов SW1 и SW2 (SW означает status word), а если в команде было закодировано значение Ne больше нуля и результат команды подразумевает ответ, то перед статусными байтами будет ещё Nr байтов, но не более Ne. Если же значение Ne было установлено в 256 (для короткого поля Le) или 65536 (для расширенного поля Le), то будут возвращены все доступные байты.

Если после возвращения результата остались ещё данные (это сигнализируется значениями статусных байтов), то используется специальный метод получения оставшихся данных, о котором я расскажу на отдельном примере.

Допустимые значения байтов SW1 и SW2 также определены в ISO 7816. Обычно принято оба этих значения записывать в виде XX YY, где XX и YY — значения SW1 и SW2 соответственно. Например, статус успешного завершения команды — 90 00, а другие значения могут сигнализировать об ошибках или необходимости выполнения дополнительных команд.

Возможные значения SW1 и SW2 обычно указываются в документациях и спецификациях. Вот как выглядит страница из спецификации PC/SC с описанием команды Read Binary:

Read Binary

❈ ❈ ❈

Такая сложная схема на самом деле сводится всего к четырём различным вариантам C-APDU.

Вариант 1:

CLAINSP1P2

Вариант 2:

CLAINSP1P2Le

В поле Le указывается максимальный размер ожидаемого ответа (от Length expected).

Вариант 3:

CLAINSP1P2Lc │ │ │ DATA │ │ │

длина DATA равна значению Lc

Вариант 4:

CLAINSP1P2Lc │ │ │ DATA │ │ │ Le

длина DATA равна значению в поле Lc, плюс в поле Le указывается максимальный размер ожидаемого ответа.

INS, CLA, P1 и т.д. — это стандартизированные названия соответствующих байтов/полей в массиве байтов APDU, на них можно ссылаться при описании различных APDU. Они фигурируют во всех стандартах, так или иначе пересекающихся с ISO/IEC 7816.

❈ ❈ ❈

Любое взаимодействие со считывателем (и соответственно с картой) происходит через отправку и получение APDU. Также у каждого считывателя бывают внутренние команды, которые не передаются на карту. Например, для управления светодиодами на корпусе используются псевдо-команды или псевдо-APDU, они используют зарезервированные CLA и INS для собственных операций.

Также через псевдокоманды происходит проксирование команд в карту, то есть считыватель предоставляет набор внутренних команд для управления отдельными видами карт. На каждую такую специальную команду он генерируется специфичные для данной карты сигналы. Псевдокоманды описаны в инструкции к считывателю.

В следующем примере я покажу, как отправлять команды и получать ответы.

example-03: Получение UID бесконтактной карты

Исходный код программы:

⭐ Для этого примера можно использовать только считыватель для бесконтактных чиповых карт. Я использую бесконтактные карты потому, что для них гарантированно можно выполнить фиксированную команду с успешным результатом, а для контактных карт такое в общем случае неверно.

Цели этого примера:

  • показать, как выглядит в коде отправка команд и получение ответов;
  • показать, как строится APDU из байтов;
  • показать успешное выполнение;
  • показать выполнение с ошибкой.

UID бесконтактной карты — это специальное глобально уникальное значение, которое присваивается каждой карте изготовителем. UID определяется в стандарте ISO 14443 и каждая удовлетворяющая ему карта должна корректно сообщать свой UID в ответ на стандартный запрос.

Вот весь код программы example-03/read-picc-uid для чтения UID бесконтактной карты:

#!/usr/bin/env python3

from smartcard.System import readers
from smartcard.CardRequest import CardRequest
from smartcard.util import toHexString

def main() -> int:
    reader = readers()[0]
    print('Connected reader: {0}'.format(reader))
    cardrequest = CardRequest(timeout=None, readers=[reader])
    print('Waiting for the card...')
    cardservice = cardrequest.waitforcard()
    cardservice.connection.connect()

    apdu = [0xFF, 0xCA, 0x00, 0x00, 0x00]
    response, sw1, sw2 = cardservice.connection.transmit(apdu)

    print('Status word: ', toHexString([sw1, sw2]))
    print('Response:', toHexString(response))

    return 0

if __name__ == '__main__':
    main()

Начало как в прошлом примере: подключаемся к первому считывателю и ждём появления карты. Дальше мы формируем APDU (напомню, это расшифровывается как application protocol data unit) в виде байтового массива и отправляем его в карту через считыватель:

apdu = [0xFF, 0xCA, 0x00, 0x00, 0x00]
response, sw1, sw2 = cardservice.connection.transmit(apdu)

Получаем ответ и распечатываем его вместе со статусным словом:

print('Status word: ', toHexString([sw1, sw2]))
print('Response:', toHexString(response))

Полностью запуск программы вместе с результатом на экране выглядит так:

% python3 example-0/read-picc-uid
Connected reader: ACS ACR122U PICC Interface
Waiting for the card...
Status word:  90 00
Response: 04 91 2A 6A 2F 64 80

Теперь разберём подробно, что же именно мы вызвали. APDU команды состоит из байтов FF CA 00 00 00:

  • 0xFF — это CLA, класс команды; значение 0xFF означает, что команда предназначена считывателю, который её преобразует в нужную последовательность сигналов при общении с картой;
  • 0xCA — это INS, код инструкции Get Data из спецификации PC/SC, раздел 3.2.2.1.3;
  • 0x00 — это параметр P1;
  • 0x00 — это параметр P2, значение P1=00 и P2=00 означает, что мы хотим получить UID карты;
  • 0x00 — это поле Le, значение 00 означает, что мы хотим получить весь результат сразу.

Таким образом, это Вариант 2 C-APDU:

CLAINSP1P2Le
FFCA000000

Значение P1=00 и P2=00 означает, что мы хотим получить UID карты.

APDU передаём через метод transmit()📚 класса smartcard.CardConnection.CardConnection. Результатом будет массив с ответом от карты и статусные байты 90 00, означающие успешное выполнение команды:

Status word:  90 00
Response: 04 91 2A 6A 2F 64 80

⭐ Для байта классаCLA значение 0xFF в стандарте ISO/IEC 7816-4 явным образом отмечено как недопустимое, то есть команды с таким классом нельзя использовать для работы с ISO-7816-картами (с контактной площадкой). Поэтому в PC/SC это значение используется для команд, предназначенных для считывателя, это описано в разделе 3.2 спецификации PC/SC part 3. Команды в этом классе обрабатываются самим считывателем и преобразуются в низкоуровневые сигналы для карты, либо вообще не отправляются на карту.

❈ ❈ ❈

В следующей программе example-03/read-picc-fail мы выполним эту же команду Get Data, однако в аргументах P1 и P2 передадим некорректные значения: 0xAB и 0xCD соответственно, она отличается от предыдущей одной строчкой:

apdu = [0xFF, 0xCA, 0xAB, 0xCD, 0x00]

И результат её выполнения такой:

% python3 example-03/read-picc-uid
Connected reader: ACS ACR122U PICC Interface
Waiting for the card...
Status word:  63 00
Response:

Вернулся пустой список байтов в качестве ответа и значение статусного слова 63 00. Это значение определено в ISO/IEC 7816-4 и означает Warning: No information given, также оно является межотраслевым, то есть его семантика зарезервирована для всех областей применения.

Теория: данные на смарт-карте, типы карт по способу управления данными

Смарт-карты (и контактные, и бесконтактные) делятся на две группы:

  • микропроцессорные карты — содержат микропроцессор и встроенное ПО для управления данными в памяти карты; они используются, например, в банковских картах, электронных паспортах, SIM-картах;
  • карты памяти — содержат только данные и, возможно, средства разграничения доступа к ним; используются, например, как электронные проездные или карты оплаты в телефонах. Такие карты существенно дешевле микропроцессорных.

Микропроцессорная карта является мини-компьютером с процессором и памятью, в этом «компьютере» могут храниться данные и запускаться приложения. Типичный пример приложения — криптографическое, которое использует хранимый в постоянной памяти смарт-карты секретный ключ для шифрования/дешифрования данных извне карты, причём снаружи карты доступа к приватному ключу нет.

Логически любая микропроцессорная карта состоит из одних и тех же компонентов, вот они на схеме вместе со связями.

ICC architecture

CPU
Главный микропроцессор карты, традиционно использовался 8-битный микроконтроллер с набором инструкций Motorola 6805 или Intel 8051, однако сейчас там может стоять гораздо более мощный (16- или даже 32-битный) процессор.
NPU
Опциональный математический сопроцессор, микроконтроллер со специфичной для какой-либо узкой задачи, например, для криптографических операций.
I/O subsystem
Подсистема ввода/вывода, через неё проходят все данные из карты и в карту.
RAM
Random access memory — оперативная память, очищается после отключения питания. Обычно размер оперативной памяти очень небольшой.
ROM
Read only memory — постоянная память, записанные на ней данные нельзя изменить, в ROM записывается, например, операционная система, криптографические и другие базовые программы.
EEPROM
Electronically erasable programmable read only memory — перезаписываемая память, содержимое которой не исчезает после отключения питания.

❈ ❈ ❈

Карта памяти (memory card, storage card) устроена существенно проще, внутри неё нет полноценного микропроцессора, а только простой недорогой чип, умеющий выполнять фиксированные несложные операции. Устроена она примерно так:

ICC architecture

I/O subsystem
Подсистема ввода/вывода, через неё проходят все данные из карты и в карту.
ROM
Read only memory — постоянная память, записанные на ней данные нельзя изменить. На картах памяти в ROM хранится, например, уникальный идентификатор карты, который присваивается при её изготовлении.
EEPROM
Electronically erasable programmable read only memory — перезаписываемая память, содержимое которой не исчезает после отключения питания. Размер этой памяти для карт данного типа обычно очень небольшой, измеряется буквально килобайтами.
Логика адресации и разграничения доступа
Это простой чип, обычно проприетарный с закрытой архитектурой, его роль состоит в обработке сигналов от подсистемы ввода-вывода и запись/чтение блоков памяти в EEPROM. Также этот чип отвечает за разграничение доступа к определённым блокам памяти.

❈ ❈ ❈

Самыми распространёнными бесконтактными картами памяти являются карты с чипами семейства Mifare (эта торговая марка принадлежит компании NXP Semiconductors — одному из крупнейших производителей чипов). Они выпускаются как в виде традиционных пластиковых карт, так и виде брелков, наклеек, колец и т.п. Самый известный пример их использования — разнообразные транспортные карты: московская «Тройка», питерский «Подорожник», транспортная карта компании «Золотая корона» и так далее.

Также существуют и контактные карты памяти, например, SLE 4418, о работе с ними будет отдельная секция. Но сначала погрузимся в детали работы с картами Mifare, будет много теории и несколько примеров, демонстрирующих различные сценарии использования различных чипов. Я специально уделяю много внимания работе с бесконтактными картами, поскольку на сегодняшний день это самый большой сегмент, применяющийся буквально везде.

example-04: определение типа бесконтактной карты памяти

Исходный код программы: example-04/detect-picc-type

Цель этого примера: показать, как можно из ATR извлечь информацию о названии карты.

Простого способа определить тип чипа и производителя карты не существует, для этого нужно на низком уровне анализировать полученные от процедуры инициализации данные. Однако в спецификации PC/SC описано, как на основании данных инициализации должно генерироваться значение ATR для бесконтактных карт (PC/SC Part 3 раздел 3.1.3.2.3), поэтому мы можем сравнительно легко определить тип протокола и чип бесконтактной карты.

Для бесконтактной карты памяти ATR содержит информацию о протоколе карты и её названии, она хранится в байтах предыстории (historical bytes). Детальный разбор структуры ATR будет далее в этой статье, сейчас же мы просто воспользуемся данными из спецификации.

ATR для бесконтактных карт памяти имеет следующую структуру:

3B 8n 80 01 80 4F LL A0 00 00 03 06 SS NN NN 00 00 00 00 TT
  • 3B — заголовок ATR, фиксированное значение
  • 8n — младший полубайт содержит длину n блока байтов предыстории, который начинается с 4 байта ATR
  • 80 — фиксированное значение
  • 01 — фиксированное значение, сразу после него начинается блок байтов предыстории длины n
    • 80 — первый байт блока байтов предыстории
    • 4F — фиксированное значение
    • LL — длина следующего байтового массива
      • A0 00 00 03 06 — фиксированное значение
      • SS — байт, кодирует стандарт карты
      • NN NN — два байта, кодируют название карты
      • 00 00 00 00 — четыре байта зарезервированы для дальнейшего использования и тут должны быть нули
  • TT — контрольная сумма ATR

Известные PC/SC значения SS и NN NN приводятся в отдельном документе спецификации, поэтому наш код по сути сводится к получению этих значений и поиску по заранее составленным таблицам.

⭐ К сожалению, этот список пополняется только по запросу производителей карт, поэтому нет гарантии, что там будет имеющаяся у вас карта, даже если она совместима с одним из стандартов. Поэтому приводимый в этом примере алгоритм будет работать только на картах, которые в списке есть.

Сначала поиск, в нём мы сразу отбрасываем карты, которые не поддерживаем:

# extract length of Historical bytes field
hb_len = atr[1] & 0xF

# extract Historical bytes
hb = atr[4:hb_len+4]

if hb[:2] != [0x80, 0x4F]:
    print('Unsupported card type')
    return 1

if hb[3:8] != [0xA0, 0x00, 0x00, 0x03, 0x06]:
    print('Unsupported card type')
    return 1

standard_byte = hb[8]
cardname_bytes = hb[9:11]

Здесь мы сохраняем значения SS и NN NN в переменных standard_byte и cardname_bytes соответственно. Чтобы эти значения преобразовать в читаемый формат, будем их искать в заранее составленных словарях, которые хранятся в переменных KNOWN_STANDARDS и KNOWN_CARD_NAMES, они достаточно большие и поэтому я их в тут не привожу.

if standard_byte not in KNOWN_STANDARDS:
    print('Unknown standard')
    return 1
print('Card standard:', KNOWN_STANDARDS[standard_byte])

cardname_word = 256*cardname_bytes[0] + cardname_bytes[1]
if cardname_word not in KNOWN_CARD_NAMES:
    print('Unknown card name:', toHexString(cardname_bytes))
    return 1
print('Card name:', KNOWN_CARD_NAMES[cardname_word])

И дальше несколько примеров работы программы на различных бесконтактных картах.

❈ ❈ ❈

Старая карта «Тройка», это классическая карта Mifare Classic 1K:

% python3 example-04/detect-picc-type
Connected reader: ACS ACR122U PICC Interface
Waiting for card...
Card connected
Card standard: ISO 14443 A, part 3
Card name: Mifare Standard 1K

❈ ❈ ❈

Новая карта «Подорожник», это более новая карта Mifare Classic 4K:

% python3 example-04/detect-picc-type
Connected reader: ACS ACR122U PICC Interface
Waiting for card...
Card connected
Card standard: ISO 14443 A, part 3
Card name: Mifare Standard 4K

❈ ❈ ❈

Бумажный билет «Единый» московского метро, это самая дешёвая и слабая карта семейства Mifare — Ultralight:

% python3 example-04/detect-picc-type
Connected reader: ACS ACR122U PICC Interface
Waiting for card...
Card connected
Card standard: ISO 14443 A, part 3
Card name: Mifare Ultra light

❈ ❈ ❈

Банковская карта Mastercard с бесконтактным чипом (это уже не карта памяти, а полноценная микропроцессорная смарт-карта):

% python3 example-04/detect-picc-type
Connected reader: ACS ACR122U PICC Interface
Waiting for card...
Card connected
Unsupported card type

❈ ❈ ❈

Важные моменты из этого примера

  • Карты памяти и микропроцессорные карты — это два совершенно разных класса устройств.
  • PC/SC предоставляет возможность сравнительно легко определить протокол и название карты памяти.
  • В бесконтактных картах отсутствует ATR, однако PC/SC его генерирует для унификации работы со всеми типа карт: и контактных, и бесконтактных.

example-05: Приложение для интерактивной работы с APDU-запросами

Исходный код программы: example-05/apdu-terminal

Для этого примера можно использовать оба типа считывателей: для бесконтактных и контактных чиповых карт.

Цели этого примера:

  • написать простое консольное приложение для интерактивной работы с картой в считывателе:
    • отправка введённых C-APDU;
    • проверка входных данных;
    • отображение ответного R-APDU;
    • обработка ошибок;
    • хранение истории команд в файле и восстановление при следующем запуске;
  • использовать дальше это приложение для интерактивных экспериментов с картой.

Такой тип приложений называется REPL (Read-Eval-Print Loop) и позволяет в интерактивном режиме работать с каким-нибудь другим приложением или оборудованием. В python уже есть встроенные средства реализации REPL и можно легко реализовать привычную функциональность: историю предыдущих команд, поиск по ней, сохранение истории с прошлых запусков, встроенную справку и так далее.

Наша программа будет работать очень просто: при запуске ждём карту в считывателе, при появлении запускаем бесконечный цикл ввода-выполнения команд. Если при выполнении команды в считывателе нет карты или возвращается ошибка от считывателя, то показываем сообщение об этом, но программу не завершаем.

Будем интерпретировать следующие команды как корректные:

FFA400000106
FF A4 00 00 01 06
FF A4 0000 0106
Это всё будет интерпретироваться как стандартная APDU-команда, после ввода APDU будет показываться в нормализованном виде: FF A4 00 00 01 06, то есть набор октетов, разделённых пробелами.
exit
quit
Эти команды будут использоваться для завершения программы. Также для выхода можно использовать стандартное для терминальных программ сочетание клавиш Ctrl+D.
FFA400000106 # select SLE card
Комментарии к командам, всё символы, начиная с # и до конца строки, будут игнорироваться. Также игнорируются лишние пробелы внутри команд и пробелы в начале и конце команды.
Также полностью игнорируем пустые команды.

Строка с приглашение для ввода будет выглядеть как APDU%, отправляемые в считыватель команды показываются с символом > в начале строки, а принимаемые со считывателя — с символом <. Ошибки будем выводит с префиксом <<< в начале строки.

Из стандартных модулей нам понадобятся:

import atexit
import readline
import os
import re

При старте инициализируем наш терминал/оболочку:

# repl initialization
history_file = os.path.expanduser('~/.apdu-terminal.history')
def terminate():
    print('Exiting...')
    readline.write_history_file(history_file)
atexit.register(terminate)
if os.path.exists(history_file):
    readline.read_history_file(history_file)
readline.set_history_length(1000)

Историю команд храним в файле ~/.apdu-terminal.history, он автоматически считывается при запуске (readline.read_history_file) и сохраняется при выходе (автоматически вызывается функция terminate при завершении программы, это делается через вызов atexit.register(terminate)). Навигация по истории клавишами-стрелками вверх/вниз.

Дальше стандартная инициализация считывателя и вывод небольшой справки:

reader = readers()[0]
print('Connected reader: {0}'.format(reader))
cardrequest = CardRequest(timeout=None, readers=[reader])
print('Waiting for card ...')
cardservice = cardrequest.waitforcard()
cardservice.connection.connect()
print('Card connected, starting REPL shell.')
print('type "exit" or "quit" to exit program, or press Ctrl+D.')

Основной цикл REPL я прокомментирую прямо в листинге кода:

while True:
    try:
        # здесь мы считываем введённую пользователем команду и указываем строку-приглашение
        cmd = input('APDU% ')
        # после получения строки выполняем её базовую нормализацию
        # удаляем весь текст, начиная с символа '#' и до конца строки
        cmd = re.sub('#.+', '', cmd)
        # удаляем пробелы в начале и конце строки
        cmd = cmd.strip()
    except EOFError:
        # здесь мы обрабатываем сочетание клавиш Ctrl+D, которое интерпретируем как выход из программы
        # выход у нас - это просто прерывание бесконечного цикла while True
        break
    if cmd in ('exit', 'quit'):
        # обрабатываем команды 'exit' и 'quit'
        break
    # здесь указываем список игнорируемых команд, пока тут только пустая строка
    if cmd in ('',):
        continue
    try:
        # пытаемся преобразовать введённую команду в байтовый массив,
        # ошибки ловятся в обработчике исключения TypeError ниже
        apdu = toBytes(cmd)
        # распечатываем нормализованный APDU-запрос
        print('>', toHexString(apdu))
        # отправляем APDU в считыватель
        response, sw1, sw2 = cardservice.connection.transmit(apdu)
        # вывод форматируем по-разному, в зависимости от длины байтового массива с ответом
        if len(response) == 0:
            print('< [empty response]', 'Status:', toHexString([sw1, sw2]))
        else:
            print('<', toHexString(response), 'Status:', toHexString([sw1, sw2]))
    except TypeError:
        print('<<< Invalid command.')
    except CardConnectionException as e:
        print('<<< Reader communication error:', str(e))

❈ ❈ ❈

Вот как выглядит сессия работы с приложением:

% ./apdu-terminal
Connected reader: ACS ACR 38U-CCID
Waiting for card ...
Card connected, starting REPL shell.
Type "exit" or "quit" to exit program, or press Ctrl+D.
APDU% FFA400000106 # select SLE card
> FF A4 00 00 01 06
< [empty response] Status: 90 00
APDU% FF  B0  00  00  20  # read some bytes from SLE card
> FF B0 00 00 20
< A2 13 10 91 FF FF 81 15 FF FF FF FF FF FF FF FF FF FF FF FF FF D2 76 00 00 04 00 FF FF FF FF FF Status: 90 00
APDU% exit
Exiting...

❈ ❈ ❈

Что ещё можно доделать в этом приложении (идеи для развития):

  • добавить встроенную справку по команде help;
  • добавить возможность выбора конкретного считывателя, если их подключено несколько;
  • добавить больше обработчиков ошибок (например, сейчас программа падает, если при запуске к системе не подключен ни один считыватель);
  • добавить обработку ситуации, когда во время работы считыватель отключается и снова подключается;
  • добавить автоматический интерпретацию известных типов ответов и их распечатку после вывода «сырого» списка байтов;
  • форматирование вывода, например, вывод байтов строками по 16 или 32;
  • добавить возможность указать дополнительные параметры при вводе APDU (например, указать, как именно нужно интерпретировать байты ответа и в каком формате отобразить).

Теория: карты памяти Mifare Classic 1K/4K

У Mifare собственный сигнальный протокол (между картой и считывателем) и собственная схема работы с данными. Эти карты не следуют стандарту ISO/IEC 7816, поэтому не понимают описанные там стандартные команды. Однако в PC/SC определены инструкции в классе FF, которые многие считыватели понимают и корректно транслируют в низкоуровневые команды для конкретного типа карты, в нашем случае — это Mifare. Полную спецификацию протокола карты можно прочитать в официальном документе производителя этих карт NXP: MIFARE Classic EV1 1K datasheet, MIFARE Classic EV1 4K datasheet.

Также NXP лицензирует свои технологии другим компаниям, которые выпускают свои карты, совместимые по протоколу с Mifare Classic. Например, это карты SLE 66R35 компании Infineon, FM1108 компании Fudan Microelectronics и другие.

❈ ❈ ❈

На карте Mifare Classic 1K один килобайт (1024 байта) в перезаписываемой памяти EEPROM, эта память разделена на 16 секторов, доступ к каждому сектору контролируется двумя разными ключами, которые обозначаются как Ключ A и Ключ B. Каждый сектор разделён на 4 блока по 16 байтов.

«Адреса» секторов начинаются с нуля: первый имеет адрес 0x00, второй — 0x01 и так далее до шестнадцатого сектора с адресом 0x0F.

У блоков адресация сквозная через сектора и начинается тоже с нуля, например, сектор 0x00 состоит из блоков с адресами {0x00, 0x01, 0x02, 0x03}, сектор 0x01 — из блоков {0x04, 0x05, 0x06, 0x07}, а сектор 0x0F — из блоков {0x3C, 0x3D, 0x3E, 0x3F}.

❈ ❈ ❈

На карте Mifare Classic 4K четыре килобайта в EEPROM-памяти, она разделена на 32 сектора по 4 блока плюс 8 секторов по 16 блоков, размер блока тоже 16 байтов, как и у Mifare Classic 1K.

«Адреса» секторов начинаются с нуля: первый имеет адрес 0x00, второй — 0x01 и так далее до 40 сектора с адресом 0x27.

У блоков адресация сквозная через сектора и начинается тоже с нуля, например, сектор 0x00 состоит из блоков с адресами {0x00, 0x01, 0x02, 0x03}, сектор 0x1F — из блоков {0x7C, 0x7D, 0x7E, 0x7F}, сектор 0x20 (первый сектор из 16 блоков) — из {0x80, 0x81, ..., 0x8F}, и, наконец, последний сектор 0x27 — из блоков {0xF0, ... 0xFE, 0xFF}.

Карта Mifare Classic 4K может использоваться везде, где используется Mifare Classic 1K — схема работы с первыми 16 секторами у них одинаковая.

❈ ❈ ❈

Первый блок памяти карты (с адресом 0x00) зарезервирован под данные производителя и содержит, в частности, UID. Теоретически он защищён от перезаписи, однако существуют специальные карты, где данные в этом блоке можно изменить.

Внешние клиенты не имеют прямого доступа к этим блокам и не могут записывать туда произвольную информацию. Также нельзя использовать все 1024/4096 байтов для записи данных, часть этого места зарезервирована для хранения ключей и условий доступа. Можно читать или писать только блок целиком (то есть 16 байт), если это позволяют условия доступа. Писать/читать отдельные байты или произвольные участки памяти нельзя.

Последний блок каждого сектора называется трейлером сектора (sector trailer) и содержит (именно в таком порядке с нулевого байта): Ключ A (6 байтов, обязательно), условия доступа (access conditions, access bits, access control bits, 3 байта), пользовательский байт (туда можно записать произвольное значение), Ключ B (6 байтов). Вместо Ключа B, если он не нужен, можно хранить любые другие данные.

Чтобы читать или писать на карту, нужно обязательно знать ключ, незащищённого/анонимного доступа карта не предоставляет. Перед любой операцией с памятью карты требуется обязательная аутентификация, она выполняется отдельно для каждого из блоков, к данным которых нужен доступ. Для аутентификации используется упомянутый ранее Ключ A (или Ключ B). Для всех операций внешние клиенты используют специальные инструкции, которые затем транслируются считывателем в протокол карты. Операция чтения/записи выполняется за три инструкции:

  1. записать в память считывателя 6 байтов ключа;
  2. выполнить команду аутентификации для нужного блока;
  3. выполнить нужную операцию с этим блоком.

PCD (Proxymity Coupling Device) транслирует C-APDU в сигнальный протокол карты, а потом так же транслирует ответ назад в R-APDU.

Допустимые операции с памятью сектора определяются условиями доступа из трейлера сектора. Для каждого блока можно установить условия доступа (на операции Read, Write, Increment, Decrement, Restore) и какой ключ использовать для аутентификации (Ключ A или Ключ B). Отдельно задаются условия доступа к блоку с трейлером (read, write).

⭐ Типичный сценарий использования, например, такой: Ключ A используется для инициализации/записи и хранится в секрете, а Ключ B используется только для чтения и поставляется вместе с программным обеспечение для использования карты.

Каждый блок в зависимости от параметров в условиях доступа может быть либо бинарным блоком (read/write block, то есть набором байтов, который можно прочитать и записать), либо числовым блоком (value block, то есть в блоке хранится целое число размером 4 байта). К числовому значению в блоке помимо операций Read и Write можно также применять операции Increment, Decrement, Transfer и Restore. Числовое значение — это четырёхбайтовое знаковое целое, в блоке может храниться только одно значение, внутри байтов блока оно повторяется три раза для контроля целостности. Считыватель обычно инкапсулирует все операции работы с числовым блоком, то есть для записи, например, вы просто в нужной команде передаёте просто число, а его кодирование в 16 байтов блока осуществляет считыватель.

Работа с картами Mifare Classic 1K выходит за границы спецификации PC/SC (и этой статьи), каждый производитель считывателей реализует собственные команды для этого. Однако коды инструкций для этих операций стараются использовать стандартные, поэтому с большой вероятностью команды будут примерно одинаковыми для разных считывателей.

❈ ❈ ❈

Условия доступа для каждого блока представляют собой три бита. Они особым образом упакованы в трёх байтах из трейлера. В спецификации подробно описан способ хранения, а также приводятся таблицы, объясняющие, как именно нужно интерпретировать различные сочетания этих битов для всех типов блоков. Подробнее о битах доступа я расскажу ниже в одном из примеров.

⭐ В блок байтов для Ключа A можно только писать данные, но нельзя читать, эти байты всегда возвращаются заполненными нулями при чтении. Блок байтов для Ключа B можно сделать читаемым определённым сочетанием битов доступа, однако в этом случае хранящиеся там данные нельзя использовать для доступа к данным в качестве ключа.

Теоретически такой подход к управлению данными позволяет добиться весьма впечатляющих результатов. Можно использовать разные наборы ключей A и B, чтобы сохранять в нужные блоки нужную информацию и безопасно её обновлять. Однако этот формат карты (точнее, некоторые ревизии карт) давно взломан и поэтому совершенно не безопасен: всю информацию (и ключи тоже) можно легко извлечь, а при желании можно сделать полную копию карты, используя специальные китайские карты-болванки.

С другой стороны, существуют способы детектирования подменных карт, которые тоже совершенствуются.

Жизненный цикл карты Mifare Classic обычно такой:

  1. На заводе производится чип со стандартными ключами и условиями доступа.
  2. Партия чипов поставляется производителю носителей.
  3. Производитель вставляет чипы в различные оболочки: карту, брелок, наклейку итп. Возможно ещё и рисует что-нибудь на карте.
  4. Карта поставляется оптовой партией сервис-провайдеру, например, метрополитену.
  5. Провайдер персонифицирует карту, то есть заполняет её нужными значениями, регистрирует в своей базе идентификатор, выставляет новые условия доступа и меняет ключи.
  6. Карта попадает к конечному пользователю.

Когда я пишу «карта», я имею в виду чип, физически носитель этого чипа может быть наклейкой, брелком и т.д.

Помимо Mifare Classic существуют более защищённые микропроцессорные карты, но у них свой протокол, устроенный совершенно по-другому. Также существуют более защищённые карты Mifare, например, Mifare Plus, у которых есть режим обратной совместимости с Mifare Classic, однако в этом режиме они точно так же уязвимы. Также есть карты с более надёжными алгоритмами, например, AES.

А теперь несколько практических примеров работы с картой Mifare Classic.

example-06: Чтение карты Mifare Classic 1K/4K

⭐ Для этого примера можно использовать только считыватель для бесконтактных чиповых карт.

Исходный код программы: example-06/read-mifare-classic

Цели этого примера:

  • подготовка считывателя для чтения данных (аутентификация) в соответствии со спецификацией PC/SC;
  • чтение данных c первого сектора карты Mifare Classic 1K/4K;
  • обработка ошибок.

Вам понадобится какая-нибудь карта Mifare Classic 1K или Mifare Classic 4K, для которой вы знаете ключ доступа к первому сектору. Идеально подойдёт чистая заводская неперсонифицированная карта. Также можете попробовать бесконтактный транспортный проездной («Тройка», «Подорожник», «Единая транспортная карта»), они часто сделаны на базе чипов Mifare Classic 1K. Даже если вы не знаете ключей доступа, то всё равно можете попробовать выполнить пример для имеющихся у вас карт, может подойти электронный пропуск, брелок от домофона или что-нибудь ещё.

На этот раз полного листинга программы тоже не будет, только фрагменты кода с пояснениями, ссылка на полный код программы в начале этого раздела.

Напомню, что в Mifare Classic 1K/4K данные представлены блоками, мы читаем весь блок сразу. Чтобы его прочитать, нам нужно знать ключ к этому сектору. Так как мы ключ не знаем, попробуем несколько стандартных вариантов, которые используются разными производителями на «чистых» картах.

Сначала зададим несколько вариантов ключей, каждый из них длиной 6 байт:

keys = [
    [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
    [0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0],
    [0xA1, 0xB1, 0xC1, 0xD1, 0xE1, 0xF1],
    [0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5],
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
]

Для каждого из ключей нам нужно выполнить две операции: загрузить ключ в память и попробовать аутентифицироваться с этим загруженным в память ключом.

Сначала загружаем ключ (он хранится в переменной key):

# 1. load key
#       CLA   INS   P1    P2    Lc      Data (6 bytes)
apdu = [0xFF, 0x82, 0x00, 0x00, 0x06] + key
response, sw1, sw2 = cardservice.connection.transmit(apdu)
if (sw1,sw2) != (0x90,0x00):
    print('Mifare Load Key failed for key {}'.format(toHexString(key)))
    continue

APDU команды выглядит так: FF 82 00 00 06 XX XX XX XX XX XX, где XX XX XX XX XX XX байты ключа из переменной key.

  • CLA = FF, напомню, что значение FF не разрешено в ISO 7816 и в этом классе передаются команды для считывателя
  • INS = 82, код инструкции Load Keys, которая определена в спецификации PC/SC Part 3, раздел 3.2.2.1.4
  • P1 = 00, набор битов, задающий структуру ключа; в данном случае 00 означает, что ключ сохраняется в волатильной памяти, то есть считыватель его забудет после отключения питания, ACR122U другие режимы не поддерживает. Иные модели считывателей могут сохранять ключ в постоянной памяти.
  • P2 = 00, номер ключа, для ACR122U здесь может стоять либо 00, либо 01. По сути это условный номер ячейки памяти считывателя, в которую записать ключ, это же значение мы передадим в следующей команде.
  • Lc = 06, длина блока данных для команды, в котором мы передаём ключ длиной 6 байтов
  • собственно данные ключа, 6 байтов

Коды ошибок для Load Keys определены в спецификации PC/SC, вы должны их все обрабатывать, если пишете нормальную программу. Однако не гарантируется, что считыватель вернёт корректную ошибку, часто он возвращает статусное слово 63 00 без уточнения, что именно пошло не так.

❈ ❈ ❈

Следующий этап — аутентификация, она выполняется для конкретного блока. Здесь используется стандартная инструкция General Authenticate, она описана в спецификации PC/SC Part 3, раздел 3.2.2.1.6.

# 2. authentication
#       CLA   INS   P1    P2    Lc    Data
apdu = [0xFF, 0x86, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x60, 0x00]
response, sw1, sw2 = cardservice.connection.transmit(apdu)
if (sw1,sw2) != (0x90,0x00):
    print('Mifare Authentication failed with key {}, status word {:02x} {:02x}'.format(toHexString(key), sw1, sw2))
    sleep(0.1)
else:
    print('Authenticated with key "{}"'.format(toHexString(key)))
    authenticated = True
    break

APDU команды выглядит так: FF 86 00 00 05 01 00 00 60 00

  • CLA = FF
  • INS = 86, команда General Authenticate
  • P1 = 00, должен стоять ноль
  • P2 = 00, должен стоять ноль
  • Lc = 05, длина данных для команды, 5 байтов
  • собственно данные, они имеют следующую структуру
    • 01 — версия, на данный момент допускается только 01
    • 00 — старший байт номера блока
    • 00 — младший байт номера блока, в нашем случае мы хотим прочитать самый первый блок, он с номером 00
    • 60 — тип ключа, который мы хотим использовать для аутентификации, 60 означает Ключ A, 61 — Ключ B.
    • 00 — номер ячейки, в которую был записан ключ в предыдущем вызове Load Keys

На этом этапе считыватель может вернуть специальную ошибку в статусном слове, если вы указали на прошлом этапе некорректный ключ. Однако это тоже не гарантируется и вы можете получить опять общий код 63 00.

⭐ Пару слов об адресации блоков. Схема со старшим и младшим байтами позволяет адресовать 65536 блоков с адресами от 0x0000 до 0xFFFF. В Mifare Classic 1K только 64 блока, поэтому старший байт всегда будет нулевым, а младший может быть в диапазоне от 0x00 до 0x3F. В Mifare Classic 4K 256 блоков, поэтому аналогично старший байт будет нулевым, а младший — в диапазоне от 0x00 до 0xFF.

⭐ Обратите внимание на код sleep(0.1), мы добавили задержку в одну десятую секунды, так как некоторые карты Mifare не позволяют проводить аутентификацию с другим ключом слишком быстро, поэтому мы перед каждой следующей попыткой ждём. Иногда это время ожидания нужно увеличивать до нескольких секунд.

Если мы подобрали нужный ключ и успешно аутентифицировались, мы можем продолжить и перейти к чтению данных из первого блока.

❈ ❈ ❈

Мы хотим прочитать тот блок, для которого проводили аутентификацию на прошлом этапе. Инструкция — Read Binary, она описана в спецификации PC/SC Part 3, раздел 3.2.2.1.8.

# 3. read data
#       CLA   INS   P1    P2    Le
apdu = [0xFF, 0xB0, 0x00, 0x00, 0x10]
response, sw1, sw2 = cardservice.connection.transmit(apdu)
if (sw1,sw2) != (0x90,0x00):
    print('Mifare read data failed, status word {:02X} {:02X}'.format(sw1, sw2))
    return 1

APDU команды выглядит так: FF B0 00 00 10

  • CLA = FF
  • INS = B0, команда Read Binary
  • P1 = 00, старший байт номера блока
  • P2 = 00, младший байт номера блока
  • Le = 10, размер ожидаемого результата, 16 байтов (10 в шестнадцатеричном представлении)

Если мы правильно угадали ключ и его можно использовать для чтения данных, то мы получим 16 байтов содержимого первого блока. Пример работы программы на карточке-пропуске из гостиницы:

% python3 ./example-06/read-mifare-classic
Connected reader: ACS ACR122U PICC Interface
Waiting for Mifare Classic 1K card...
Card connected
Mifare Authentication failed with key A0 B0 C0 D0 E0 F0, status word 63 00
Mifare Authentication failed with key A1 B1 C1 D1 E1 F1, status word 63 00
Mifare Authentication failed with key A0 A1 A2 A3 A4 A5, status word 63 00
Authenticated with key "FF FF FF FF FF FF"
Got response: 13 85 17 6C ED 08 04 00 01 25 AC 59 3B B3 45 1D

На карте Mifare в первом блоке хранится служебная информация, он обычно защищён от перезаписи. В частности, там записан UID.

Вы можете поэкспериментировать с кодом, например, чтобы вернуть содержимое второго блока (с адресом 0x0001), отправьте в третьем этапе команду с таким APDU: FF B0 00 01 10. Обратите внимание, что после успешной аутентификации инструкция Read Binary будет использовать этот же ключ для всех попыток доступа к любому из четырёх блоков этого же сектора. Поэтому вы можете аутентифицироваться для блока 0x0000 и пытаться читать данные из остальных трёх блоков без необходимости проводить аутентификацию заново. Однако читать блоки из другого сектора уже не получится.

В обычных картах (например, транспортных) для хранения данных используется только несколько секторов, а для первого сектора часто оставляется стандартный ключ доступа. Например, в карте «Тройка» Ключ А первого сектора: A0 A1 A2 A3 A4 A5, а у карты «Подорожник» — FF FF FF FF FF FF.

❈ ❈ ❈

Важные моменты из этого примера

  • В четвёртом блоке каждого сектора хранятся ключи (только для этого сектора!) и условия доступа для всех четырёх блоков сектора.
  • Всего в секторе можно хранить два ключа: Ключ A и Ключ B.
  • Для каждого блока можно установить свои параметры доступа.
  • Для любых операций с данными на карте Mifare Classic требуется предварительная аутентификация для конкретного блока, после чего можно без аутентификации получить доступ к остальным блокам сектора (с этим же ключом).
  • Аутентификация выполняется в два этапа: сначала ключ загружается в считыватель, а затем производится аутентификация со ссылкой на ранее загруженный ключ.
  • Ключ живёт в считывателе до его отключения, если он был загружен в волатильную память.
  • Если загрузить ключ в неволатильную память, он будет доступен и после отключения.
  • В ACR122U можно загрузить одновременно только два ключа и только в волатильную память.
  • У каждого считывателя есть особенности, поэтому обязательно читайте документацию.
  • Для новой карты ключи A и B установлены в значения по умолчанию, разные для разных производителей.

example-07: Разбор данных на карте Mifare Classic 1K/4K

⭐ Для этого примера можно использовать только считыватель для бесконтактных чиповых карт.

Исходный код программы: example-07/dump-mifare-classic

Цели этого примера:

  • путём перебора фиксированного набора ключей попытаться прочитать все блоки;
  • разобрать условия доступа и вывести на экран;
  • вывести на экран содержимое блоков, которые удалось прочитать.

По сути этот пример является чуть усложнённой версией предыдущего. Схема программы примерно такая:

  • определить тип карты и вывести ошибку, если это не Mifare Classic 1K/4K;
  • задать список констант-ключей;
  • пройтись по всем секторам карты и попробовать аутентифицироваться на каждом всеми предопределёнными ключами;
  • попытаться прочитать содержимое всех блоков при успешной аутентификации;
  • попытаться прочитать условия доступа для каждого сектора;
  • красиво изобразить результат на экране в псевдографике:
    • показать для каждого из секторов, какой ключ обнаружен (A, B, A и B, никакой);
    • показать данные для каждого блока, если их удалось прочитать;
    • показать биты доступа для каждого блока.

В примерe 4 мы определяли тип карты, в этом же мы не будем этого делать, а сразу предполагаем, что в считывателе карта именно Mifare 1K/4K. Будем пытаться прочитать только первые 16 секторов.

Целиком текст программы приводить не буду, так как он по большей части состоит из кода для красивой распечатки прочитанных данных. Но несколько функций стоит отметить.

Вот функция для парсинга байтов доступа в массив битов доступа:

def unpack_access_conditions_bits(ac_bytes):
    bit = lambda pos, b: ((b >> pos) & 1)
    #ac0 = ac_bytes[0]  # we don't need that byte
    ac1 = ac_bytes[1]
    ac2 = ac_bytes[2]
    return [
        [bit(4, ac1), bit(0, ac2), bit(4, ac2)],
        [bit(5, ac1), bit(1, ac2), bit(5, ac2)],
        [bit(6, ac1), bit(2, ac2), bit(6, ac2)],
        [bit(7, ac1), bit(3, ac2), bit(7, ac2)]
    ]

На вход передаём три байта из трейлера сектора, в которых закодированы условия доступа к блокам сектора; на выходе получаем массив из битов доступа (три бита) для каждого из блоков.

Далее функции, которые по битам доступа определяют, что можно сделать с блоком:

def can_read_block_with_key_a(ac_bits):
    return ac_bits in [ [0,0,0], [0,1,0], [1,0,0], [1,1,0], [0,0,1] ]


def can_read_key_b_bytes(ac_bits):
    return ac_bits in [ [0,0,0], [0,0,1], [0,1,0] ]

❈ ❈ ❈

Результат работы программы выглядит примерно так (использована карта-проездной Золотая Корона):

% ./dump-mifare-classic
Connected reader: ACS ACR122U PICC Interface
Waiting for Mifare Classic 1K/4K card...
Card connected, reading data, please wait...
Reading sectors: ................
Sector 00
  block 00: 41 A7 13 48 FF 88 04 00 43 25 CC 15 00 18 0C 05, Key A: A0 A1 A2 A3 A4 A5, Key B: ?? ?? ?? ?? ?? ??, AC: 1 0 0
  block 01: 0C 0F 18 FF 00 00 00 00 18 EE 00 00 18 EE 00 00, Key A: A0 A1 A2 A3 A4 A5, Key B: ?? ?? ?? ?? ?? ??, AC: 1 0 0
  block 02: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05, Key A: A0 A1 A2 A3 A4 A5, Key B: ?? ?? ?? ?? ?? ??, AC: 1 0 0
  block 03: 00 00 00 00 00 00 78 77 88 83 00 00 00 00 00 00, Key A: A0 A1 A2 A3 A4 A5, Key B: ?? ?? ?? ?? ?? ??, AC: 0 1 1
Sector 01
  block 04: ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??, Key A: ?? ?? ?? ?? ?? ??, Key B: ?? ?? ?? ?? ?? ??, AC: ? ? ?
  block 05: ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??, Key A: ?? ?? ?? ?? ?? ??, Key B: ?? ?? ?? ?? ?? ??, AC: ? ? ?
  block 06: ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??, Key A: ?? ?? ?? ?? ?? ??, Key B: ?? ?? ?? ?? ?? ??, AC: ? ? ?
  block 07: ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??, Key A: ?? ?? ?? ?? ?? ??, Key B: ?? ?? ?? ?? ?? ??, AC: ? ? ?
Sector 02
  block 08: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF, Key A: FF FF FF FF FF FF, Key B: FF FF FF FF FF FF, AC: 0 0 0
  block 09: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF, Key A: FF FF FF FF FF FF, Key B: FF FF FF FF FF FF, AC: 0 0 0
  block 0A: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF, Key A: FF FF FF FF FF FF, Key B: FF FF FF FF FF FF, AC: 0 0 0
  block 0B: 00 00 00 00 00 00 FF 07 80 D2 FF FF FF FF FF FF, Key A: FF FF FF FF FF FF, Key B: FF FF FF FF FF FF, AC: 0 0 1
Sector 03
...
и остальные секторы

На схеме ниже показано, как устроен один сектор, его трейлер и как упакованы биты доступа для четвёртого блока сектора:

aaa

По сути каждый бит продублирован в инвертированном виде и благодаря такой избыточности все 12 битов доступа упакованы в 24 бита в трёх байтах. В позиции, помеченной как Байты ключа A, можно записать ключ A, но при чтении здесь всегда будут нули. В позиции, помеченной как Байты ключа B, можно записать Ключ B (в этом случае эти байты при чтении будут содержать нули), либо произвольные шесть байтов, режим определяется битами доступа.

Сочетание битов C1, C2 и C3 для первых трёх блоков сектора определяет следующие условия доступа:

C1C2C3Что можно делать (остальное нельзя)
000Разрешены все операции для ключей A или B
010Read(А,B)
100Read(A,B), Write(B)
110Read(A,B), Write(B), Increment(B), Decrement/Transfer/Restore(A,B)
001Read(A,B), Decrement/Transfer/Restore(A,B)
011Read(B), Write(B)
101Read(B)
111Запрещены все операции для обоих ключей

Здесь запись Read(A,B) означает, что разрешено чтение блока ключом A или B, Write(B) — разрешена запись блока только ключом B и так далее. Напомню, что блок в картах mifare classic пишется только целиком.

Как видно из этой таблицы, максимальные возможности есть у Ключа B: практически всегда можно читать данные Ключом B, а Ключ A используется преимущественно для чтения.

Сочетание битов C1, C2 и C3 для трейлера сектора определяет следующие условия доступа до блока с трейлером:

C1C2C3Что можно делать (остальное нельзя)
000WriteA(A), AccessRead(A), ReadB(A), WriteB(A)
010AccessRead(A), ReadB(A)
100WriteA(B), AccessRead(A,B), WriteB(B)
110AccessRead(A,B)
001WriteA(A), AccessRead(A), AccessWrite(A), ReadB(A), WriteB(A)
011WriteA(B), AccessRead(A,B), AccessWrite(B), WriteB(B)
101AccessRead(A,B), AccessWrite(B)
111AccessRead(A,B)

Здесь WriteA(B) означает, что можно ключом B перезаписать ключ A, ReadB(A) означает, что можно ключом A прочитать ключ B, AccessRead(A) можно читать байты контроля доступа ключом A и так далее.

Обратите внимание, что использовать Ключ B для контроля доступа можно только при условии, что условия доступа на трейлер не позволяют Ключ B прочитать. Другими словами, если вы разрешите чтение байтов для хранения Ключа B, то вы не сможете использовать эти байты для чтения/записи других блоков сектора, даже если там разрешён доступ при помощи Ключа B. Ключ A нельзя прочитать никогда. Биты доступа можно прочитать всегда либо ключом A, либо ключом B, если этот ключ разрешён к использованию. Если какой-то из элементов запрещено читать, то в нём возвращаются нули.

❈ ❈ ❈

При помощи этих таблиц вы можете теперь читать последнюю колонку в результатах работы программы. Например, набор битов 1 0 0 для первых трёх блоков означает Read(A,B), Write(B), то есть данные в блоке можно читать ключом A или B, а записать можно только ключом B. А набор битов 0 1 1 в блоке трейлера означает WriteA(B), AccessRead(A,B), AccessWrite(B), WriteB(B), то есть можно при помощи ключа B записать ключ A или B, прочитать биты доступа можно ключом A или B, но записать биты доступа можно только ключом B.

Таблицы со структурой битов доступа созданы на основе данных из спецификации на карты Mifare Classic, смотрите разделы 8.7.2 Access conditions for the sector trailer и 8.7.3 Access conditions for data blocks.

❈ ❈ ❈

Существует также сочетание битов доступа, которое в спецификации называется транспортным (transport), для блоков данных это 0 0 0, для трейлера — 0 0 1. Это считается изначальной конфигурацией для новой карты, в этом виде она поставляется с завода-изготовителя. В этой конфигурации ключом A можно читать и писать все блоки, читать и писать биты доступа и байты ключа B, а также записывать байты ключа A.

Важные моменты из этого примера

  • Фактически пользователю доступно 768 байтов из 1024, остальное выделено под служебную информацию.
  • Подбор ключей по такой схеме — очень неэффективная операция.
  • Неправильной установкой битов доступа можно полностью отрезать себе доступ к блоку.
  • Новая «заводская» карта использует стандартные ключи, которые зависят от производителя.
  • Некоторые карты снабжены защитой от перебора ключей, для них операция General Auth будет возвращать ошибку в течение некоторого интервала времени, если предыдущая попытка была неудачной. Чтобы это обойти, нужно вставить в код искусственную задержку в пару секунд перед каждой операцией General Auth.

example-08: Практический пример: использование карты Mifare Classic 1K/4K как платёжной

⭐ Для этого примера можно использовать только считыватель для бесконтактных чиповых карт.

Также я рекомендую для экспериментов использовать новую чистую карту Mifare Classic 1K без данных.

Цели этого примера:

  • планирование структуры размещения данных;
  • демонстрация записи данных на карту памяти Mifare Classic 1K:
  • запись ключей;
  • запись данных;
  • запись условий доступа.

В самом начале я рассказывал коротко о жизненном цикле смарт-карты. Персонализация — это подготовка карты перед выдачей её клиенту. В этом примере мы напишем код для персонализации, а также для «реального» использования карты. В качестве базовой модели возьмём электронный проездной: на карте записано, сколько там хранится денег. А считыватель для бесконтактных карт будет исполнять разные роли в зависимости от запущенной программы: инициализация карты, пополнение карты, списывание денег.

Итак, требования к нашему проездному:

  • на карте хранится баланс в виде целого положительного числа;
  • считыватель в роли «эмитент» (issuer) может «выпустить» карту:
    • в определённый сектор записываются ключи доступа;
    • для сектора задаются права доступа на трейлер и первый блок сектора;
    • в первый блок записывается нулевой баланс;
  • считыватель в роли «кассир» может выполнять следующие операции:
    • прочитать и показать текущий баланс карты;
    • увеличить баланс на определённую величину;
  • считыватель в роли «кондуктор» может списать фиксированную сумму, то есть уменьшить записанный баланс на некоторое число;
  • считыватель в роли «эмитент» может «уничтожить» карту, то есть вернуть весь сектор в исходное состояние, удалить записанные данные и выставить начальные ключи и условия доступа.

Для каждой из ролей у нас будет использоваться отдельное приложение. Все приложения будут использовать общий конфигурационный файл, в котором будут заданы следующие параметры:

  • номер сектора и блока для хранения суммы баланса;
  • исходный ключ для этого сектора для чистой карты;
  • ключи для этого сектора, которые будут использоваться для работы с балансом;
  • «стоимость» поездки, то есть сколько денег будет сниматься с карты при её использовании для оплаты.

Помимо собственно программ в каталоге этого примера лежит файл config.ini, в котором записаны все параметры нашей системы:

  • номер сектора и блока в нём, который мы используем для хранения данных;
  • исходный ключь A к этому сектору;
  • новые ключи доступа A и B;
  • величина единичного списания баланса при использовании карты для оплаты;
  • данные хранятся в числовом блоке (value block), для доступа к ним используются специфичные для mifare classic операции работы с такими блоками.

Далее подробно о каждой из программ этого примера. Как и прежде, я не буду приводить полный листинг, а также не буду приводить сервисный код для работы с файлом конфигурации.

issue-card

Исходный код программы: example-08/issue-card

Эта программа делает персонализацию, то есть подготавливает новую чистую смарт-карту для использования в нашей системе. Для этого мы в фиксированный сектор карты запишем собственные ключи и первоначальный баланс 0 единиц в выбранный числовой блок. Для управления балансом будем пользоваться операциями Increment (пополнение, refille) и Decrement (списание, checkout).

Мы будем использовать ключ B для «административного» доступа: пополнение карты, очистка (отзыв) карты. А ключ A будем использовать исключительно для списывания и проверки баланса. При таком сценарии ключ B «знают» только управляющие считыватели в кассах, а ключ A записан на всех транспортных считывателях.

Инициализация проходит штатно: читаем из конфига нужные данные (номер сектора, блока внутри сектора, оригинальный ключ A) и дальше пытаемся с ним аутентифицироваться к последнему блоку сектора. Пытаемся прочитать трейлер сектора и проверить биты доступа к нему:

# try to read trailer block content
#       CLA INS BlockMSB BlockLSB Le
apdu = 'FF  B0  {:02X}   {:02X}   10'.format(blockMSB, blockLSB)
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('failed to read sector trailer data')
    return 1
# check access condition, we should be able to change both access keys and write access bits using current Key A
# the only bits combination for this is `0 0 1`
access_conditions_bits = unpack_access_conditions_bits(response[6:9])
if access_conditions_bits[3] != [0, 0, 1]:
    print('unsuitable access bits')
    return 1
print('done')

В модуле util.py у нас определены функции для распаковки и запаковки байтов доступа: unpack_access_conditions_bits и pack_access_conditions_bits. unpack_access_conditions_bits возвращает список из четырёх элементов для каждого из блоков, в каждом элементе также список из трёх элементов, где могут быть или 0, или 1. Мы требуем, чтобы для блока с трейлером изначально был установлен набор битов доступа 0 0 1.

Если проверка прошла успешно, генерируем новый трейлер, состоящий из нового ключа A, новых байтов доступа и нового ключа B. Для выбранного блока-значения (где хранится баланс) выставляем биты доступа 1 1 0, для блока-трейлера — 0 1 1. Такие значения позволят нам использовать ключ B в качестве главного административного (при помощи него можно пополнять карту, а также приводить выбранный сектор к исходному транспортному состоянию), а ключ A используется только для запроса баланса и операции списания фиксированной суммы с баланса.

print('Initializing sector trailer ... ', end='')
# new access bit for trailer should be `0 1 1` (write Keys A and B using key B, read access bit with both keys,
# write access bits using Key B)
access_conditions_bits[3] = [0, 1, 1]
# new access bit for value block should be `1 1 0` (allow read and decrement with key A, all other operations
# with key B)
access_conditions_bits[value_block] = [1, 1, 0]
trailer_bytes = toBytes(key_a) + pack_access_conditions_bits(access_conditions_bits) + [0] + toBytes(key_b)
# write bytes to trailer block
#       CLA INS BlockMSB BlockLSB Lc  DATA
apdu = 'FF  D6  00       {:02X}       10 {}'.format(value_sector * 4 + 3, toHexString(trailer_bytes))
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('sector trailer block write failed')
    return 1
print('done')

Для записи значения блока целиком используем APDU с инструкцией 0xD6, напомню, что как и остальные команды для работы с картами Mifare, эти инструкции реализуются внутри считывателя (в нашем случае это ACS ACR122U) и поэтому для разных устройств они могут отличаться. Обычно они все описаны в документации к считывателю, для нашего можно посмотреть тут. Команда с кодом инструкции 0xD6 описывается в разделе 5.4. Update Binary Blocks.

❈ ❈ ❈

После записи трейлера нам необходимо заново аутентифицироваться, на этот раз при помощи нового ключа B. После аутентификации мы инициализируем числовой блок и записываем туда 0.

print('Initializing value block ... ', end='')
balance = 0
nb = toHexString(list(balance.to_bytes(4)))
# write balance (0) to value block
#       CLA INS P1 P2     Lc VB_OP ValueMSB...LSB
apdu = 'FF  D7  00 {:02X} 05 00    '.format(value_sector * 4 + value_block) + nb
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('value block write failed')
    return 1
print('done')

Для записи числового блока используется команда с кодом инструкции 0xD7, которая описана в разделе 5.5.1. Value Block Operation упомянутой выше документации к считывателю. Команда принимает номер блока в поле P2, а в блоке данных передаётся пять байтов (в поле Lc стоит значение 0x05): сначала код операции (в нашем случае это 0x00 — запись значения) и далее целое числовое значение, закодированное в виде четырёх байтов (big endian). Для преобразования числа в список байтов мы используем такой код: list(balance.to_bytes(4)).

refill-card-balance

Исходный код программы: example-08/refill-card-balance

В этой программе мы пополняем баланс карты на фиксированное значение. Для пополнения используем штатную команду Mifare Increment, а для аутентификации используем ключ B.

# refill balance, add 50 units
n = 50
print('Incrementing balance by {} units ... '.format(n), end='')
nb = toHexString(list(n.to_bytes(4)))
#       CLA INS P1 P2     Lc VB_OP ValueMSB...LSB
apdu = 'FF  D7  00 {:02X} 05 01  '.format(value_sector * 4 + value_block) + nb
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('value block increment failed')
    return 1
print('done')

Для инкремента используем ту же инструкцию 0xD7, только в качестве кода операции указываем 0x01, значение для инкремента аналогично передаём в последних четырёх байтах блока данных.

check-card-balance

Исходный код программы: example-08/check-card-balance

В этой программе мы аутентифицируемся ключом A и считываем баланс из блока-значения:

# read balance
blockLSB = value_sector * 4 + value_block
#       CLA INS P1 P2     Le
apdu = 'FF  B1  00 {:02X} 04'.format(blockLSB)
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('authentication failed for trailer block of sector {:02X}'.format(value_sector))
    return 1
print('new balance is {} units'.format(int.from_bytes(bytes(response))))

Инструкция этой команды — B1 — также описана в документации к считывателю в разделе 5.5.2. Read Value Block. Там всё просто, в поле P1 передаём 0x00, а в поле P2 — номер блока с данными. В результате получаем значение, закодированное в четырёх байтах, для обратного преобразования его в целое значение используем код int.from_bytes(bytes(response)).

checkout

Исходный код программы: example-08/checkout

В этой программе мы списываем с карты фиксированную стоимость «поездки» при каждом прикладывании. Чтобы имитировать работу транспортного считывателя, мы запускаем код для обработки карты в бесконечном цикле, и теперь с карты будет «списываться» сумма при каждом поднесении её к считывателю. Если на карте недостаточно денег, показывается соответствующее предупреждение.

Чтобы такой подход с бесконечным циклом работал, необходимо немного изменить схему инициализации объектов: указываем дополнительный аргумент newcardonly=True, чтобы вызов cardservice = cardrequest.waitforcard() ждал, когда карту сначала уберут от считывателя.

cardrequest = CardRequest(newcardonly=True, timeout=None, readers=[reader])

Остально то же самое: считываем баланс, проверяем, что он больше или равен суммы списывания. Если всё хорошо, то выполняем операцию декремента:

print('Debit {} units from your balance ... '.format(single_checkout_value), end='')
nb = toHexString(list(single_checkout_value.to_bytes(4)))
#       CLA INS P1 P2     Lc VB_OP ValueMSB...LSB
apdu = 'FF  D7  00 {:02X} 05 02  '.format(value_sector * 4 + value_block) + nb
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('value block decrement failed')
    continue
print('done')

Декремент выполняется также через команду с инструкцией 0xD7, только код операции теперь указываем 0x02.

revoke-card

Исходный код программы: example-08/revoke-card

Эта программа очищает используемый нами сектор, сбрасывает ключи и биты доступа до транспортного состояния. Здесь тоже всё аналогично: аутентифицируемся с ключом B, формируем транспортную конфигурацию для блока с трейлером, записываем нули в блок с данными и записываем блок с трейлером.

# fill data block with zeroes
block_bytes = '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
# write bytes to trailer block
#       CLA INS BlockMSB BlockLSB Lc  DATA
apdu = 'FF  D6  00       {:02X}       10 {}'.format(value_sector * 4 + value_block, block_bytes)
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('sector data block write failed')
    return 1
print('done')

access_conditions_bits = [
    [0, 0, 0],
    [0, 0, 0],
    [0, 0, 0],
    [0, 0, 1]
]
print('Restore transport state of sector ... ')
trailer_bytes = toBytes(original_key_a) + pack_access_conditions_bits(access_conditions_bits) + [0] + toBytes('FF FF FF FF FF FF')
# write bytes to trailer block
#       CLA INS BlockMSB BlockLSB Lc  DATA
apdu = 'FF  D6  00       {:02X}       10 {}'.format(value_sector * 4 + 3, toHexString(trailer_bytes))
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('sector trailer block write failed')
    return 1
print('done')

Теория: карты Mifare Ultralight

Карты стандарта Mifare Ultralight сделаны на основе очень дешёвых чипов и предназначены для хранения небольшого (40-144 байтов) объёма данных. Владелец бренда и спецификаций — компания NXP.

На данный момент семейство Mifare Ultralight состоит из следующих чипов:

MIFARE Ultralight
Кодовое имя MF0ICU1, EEPROM 512 бит (16 страниц по 4 байта), без аутентификации, даташит. На данный момент эти чипы сняты с производства.
MIFARE Ultralight Nano
Кодовое имя MF0UN(H)00, EEPROM 448 (14 страниц по 4 байта), без аутентификации, обратно совместима с MF0ICU1, даташит.
MIFARE Ultralight C
Кодовое имя MF0ICU2, EEPROM 1536 бит (36 страниц по 4 байта), 3DES аутентификация, обратно совместима с MF0ICU1 при работе с первыми 512 битами, даташит.
MIFARE Ultralight EV1
Кодовое имя MF0ULX1, EEPROM 640 бит (версия MF0UL11, 20 страниц по 4 байта) или 1312 бит (версия MF0UL21, 41 страница по 4 байта), защита паролем для предотвращения изменений памяти, обратно совместима с MF0ICU1 при работе с первыми 512 битами, даташит.

❈ ❈ ❈

Память разбита на страницы (pages), на каждой четыре байта, ниже в таблице показана её структура. Страницы нумеруются с нуля.

Страницабайт 00байт 01байт 02байт 03комментарии
00SN0SN1SN2BCC0UID
01SN3SN4SN5SN6UID
02BCC1internalLock0Lock1блокировка записи в пользовательских байтах
03OTP0OTP1OTP2OTP3байты однократной записи
04пользовательские байты
...
XXпользовательские байтыобщее количество страниц зависит от типа карты

У каждой карты уникальный серийный номер (UID) длиной 7 байт, закодированный в первых девяти байтах памяти, на схеме байты UID обозначены ячейками от SN0 до SN6, в ячейках BCC0 и BCC1 хранятся байты проверки Check Byte0 и Check Byte1 соответственно (см. стандарт ISO/IEC 14443-3). Кроме того, SN0 кодирует производителя чипа (Manufacturer ID, см. ISO/IEC 14443-3 и ISO/IEC 7816-6 AMD.1), для NXP Semicondutors выделено значение 04, остальные коды можно посмотреть, например, тут.

В байтах Lock0 и Lock1 закодированы биты блокировки (lock bits), через них можно заблокировать страницы с 03 до 0E от перезаписи, после установки указанные страницы блокируются от записи и доступ к ним возможен только на чтение. Битовое представление этих двух байтов следующее:

L7 L6 L5 L4 LOTP BL15-10 BL9-4 BLOTP
L15 L14 L13 L12 L11 L10 L9 L8

В битах с L4 по L15 можно записать 1, чтобы заблокировать запись в соответствующую страницу. Бит LOTP отвечает за блокировку записи в страницу 3 с OTP-байтами. Бит BL15-10 отвечает за блокировку битов с L10 по L15, бит BL9-4 отвечает за блокировку битов с L4 по L9, а бит BLOTP позволяет заблокировать запись в бит LOTP.

После записи 1 в любой из этих битов изменить его больше нельзя и соответствующий блок остаётся заблокированным на запись.

❈ ❈ ❈

В байтах OTP0 .. OTP3 (OTP расшифровывается как One-Time Programmable) можно записать произвольные битовые значения, но только один раз. После записи значения 1 в один из 32 битовых регистров этот регистр блокируется и далее его изменить нельзя. Запись 0 в регистр его не меняет.

Чтение и запись на карту производится блоками-страницами, за раз можно прочитать и записать одну страницу.

❈ ❈ ❈

В даташитах на конкретную модель карты можно посмотреть, какие блоки памяти выделены под специальные данные. Например, в них можно записывать пароль или ключ доступа, включать специальные возможности (счётчик подключений, например). Такие блоки располагаются в конце. Например, для Mifare Ultralight C — это блоки с номерами от 0x29 (41) до 0x2F (47) включительно.

Во всех модификациях Mifare Ultralight первые страницы с 0x04 (4) по 0x0F (15) включительно (нумерация с нуля) можно защитить от перезаписи, а страница 3 выделена для OTP (One-Time Programmable).

example-09: Практический пример: использование карты Mifare Ultralight

⭐ Для этого примера можно использовать только считыватель для бесконтактных чиповых карт.

Для тестов вы можете взять бумажный одноразовый проездной московского метро или купить брелки/наклейки Mifare Ultralight. Для детектирования типа карт можете пользоваться программой detect-picc-type из примера example-04.

Цели этого примера:

  • продемонстрировать, как кодируются данные в карте Mifare Ultralight;
  • прочитать и вывести на экран содержимое памяти, разбитое на страницы;
  • отметить страницы, заблокированные от записи, и другую информацию с карты.

read-mifare-ultralight

Исходный код программы: example-09/read-mifare-ultralight

Распечатаем содержимое памяти и пометим страницы, доступные только для чтения. Содержимое страниц будем хранить в переменной pages. Для начала прочитаем первую страницу, если это не получится, то в считывателе не карта Mifare Ultralight.

# read first page to check type
#       CLA INS P1  P2      Lc
apdu = 'FF  B0  00  {:02X}  04'.format(page)
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Read Binary failed, probably not Mifare Ultralight card, terminating.')
    return 1
pages.append(response)
page += 1
print('done')

Для чтения страницы используется уже знакомая нам инструкция B0 Read Binary. Аутентификация не нужна, просто сразу читаем блок из четырёх байтов. Если смогли прочитать, сохраняем этот блок в списке pages и читаем остальные в бесконечном цикле, пока не возникнет ошибка:

while True:
    #       CLA INS P1  P2      Lc
    apdu = 'FF  B0  00  {:02X}  04'.format(page)
    response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
    if (sw1,sw2) != (0x90,0x00):
        break
    pages.append(response)
    print('.', end='', flush=True)
    page += 1

Далее извлекаем из прочитанных байтов блоки с идентификатором (смотрим таблицу из прошлого раздела):

uid_bytes = pages[0][0:3] + pages[1]

И также считываем заблокированные для записи номера страниц (также смотрим на таблицу, как распределены биты для обозначения страниц):

lock_0 = byte_to_bin(pages[2][2])
lock_1 = byte_to_bin(pages[2][3])
locked_pages = {}
for x in range(5):
    if lock_0[x] == 1:
        locked_pages[7-x] = True
for x in range(8):
    if lock_1[x] == 1:
        locked_pages[15-x] = True

И в итоге просто распечатываем содержимое всех страниц с дополнительными пометками:

print('Page | Memory bytes | State ')
print('-----+--------------+---------')
for i,p in enumerate(pages):
    print('  {:02X} | {}  | '.format(i, toHexString(p)), end='')
    if locked_pages.get(i, False) == True:
        print('locked')
    else:
        print('')

Результат работы программы на бумажном проездном из московского метро выглядит так:

% ./read-mifare-ultralight
Connected reader: ACS ACR122U PICC Interface
Waiting for Mifare Ultralight card ...
Card connected, checking card, please wait ... done
Reading pages ................... 20 pages
UID: 34ABFAA114EC07
OTP bits: [1, 1, 1, 1, 1, 1, 1, 1] [1, 1, 1, 1, 1, 1, 1, 1] [1, 1, 1, 1, 1, 1, 1, 1] [1, 1, 1, 1, 1, 1, 0, 0]
Lock bits: [1, 1, 1, 1, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0]
Page | Memory bytes | State
-----+--------------+---------
  00 | 34 AB FA ED  |
  01 | A1 14 EC 07  |
  02 | 5E 00 70 08  |
  03 | FF FF FF FC  |
  04 | 45 D9 BC 20  | locked
  05 | F6 C7 2A 00  | locked
  06 | 81 E0 38 40  | locked
  07 | 00 B1 E0 01  |
  08 | 00 05 A1 40  |
  09 | 00 00 00 00  |
  0A | A6 7B 53 47  |
  0B | 81 E0 38 40  | locked
  0C | 00 B1 E0 01  |
  0D | 00 05 A1 40  |
  0E | 00 00 00 00  |
  0F | A6 7B 53 47  |
  10 | 00 00 00 00  |
  11 | 00 00 00 00  |
  12 | 00 00 00 00  |
  13 | 00 00 00 00  |

Теория: NFC-метки

NFC-метки, они же NFC-карты, NFC-теги (NFC tags) — это устройства, удовлетворяющие стандартам ISO/IEC 14443 и одновременно спецификациям NFC Forum, сообществу организаций по унификации и стандартизации использования беспроводных технологий NFC (Near Field Communication). Если совсем упростить, то NFC-устройства предназначены для передачи структурированных данных (описанных в спецификациях NFC Forum) через радиоканал, например, в смартфоны. Либо для обмена информацией между смартфонами и считывателями. На метке может быть записан пароль к Wi-Fi сети, параметры подключения к Bluetooth, дополнительные данные к товару, адрес сайта, платёжная информация и так далее. Компания Nintendo использует NFC-метки для производства фигурок-компаньонов Amiibo к своим играм. Иногда такие устройства без собственного источника питания называют пассивными метками (passive tags).

Также в роли NFC-устройства может выступать смартфон, так происходит при работе платёжных сервисов типа Google Pay. Такие устройства называют активными метками (active tags). Разделение на пассивные и активные метки достаточно условное, поскольку внутри NFC метки может находиться достаточно сложная микросхема, которая даже на операции чтения может модифицировать хранимые данные, например, увеличивать счётчик количества чтений карты.

Все аспекты использования NFC описаны в технических спецификациях NFC Forum, которые, к сожалению, не являются публичными, но при желании вы сможете их найти в интернете.

❈ ❈ ❈

Упрощённо стек NFC можно описать такой схемой:

NFC tech stack

Более полную схему можно посмотреть в википедии: NFC Protocol Stack.png.

❈ ❈ ❈

NFC как спецификация базируется на стандартах ISO/IEC 14443, FeliCa и других. На данный момент существуют следующие типы NFC-устройств по типу используемого радиочастотного интерфейса (все они работают в диапазоне 13,56 МГц):

  • NFC A, на основе Type A из стандарта ISO 14443-3
  • NFC B, на основе Type B из стандарта ISO 14443-3
  • NFC F, на основе JIS 6319-4 (японский стандарт FeliCa, разработан Sony)
  • NFC V, на основе ISO 15693 (vicinity cards, с гораздо бо́льшим расстоянием считывания, примерно до полутора метров)

В спецификациях NFC Forum на данный момент описаны пять типов NFC-меток:

NFC Type 1 Tag
Метки на основе NFC A, самые простые и дешёвые, однако с 2021 года этот тип исключён из спецификаций NFC Forum в пользу NFC Type 2 Tag. Память разделена на блоки по 8 байтов, всего блоков 15 или больше.
NFC Type 2 Tag
Метки на основе NFC A, от NFC Type 1 Tag отличаются наличием механизмов защиты от конфликтов и коллизий при записи. На данный момент это самый распространённый тип NFC-меток. Память разделена на блоки по 4 байта, всего блоков 16 или больше.
NFC Type 3 Tag
Метки на основе NFC F (FeliCa). Память разделена на блоки по 16 байтов, количество блоков не фиксировано, прямой адресации к блокам нет, вместо этого используется группировка по сервисам (service).
NFC Type 4 Tag
Метки на основе NFC A или NFC B. Прямого доступа к памяти нет, вместо этого для обмена данными используется протокол ISO Data Exchange Protocol (ISO-DEP) и набор APDU-команд согласно ISO/IEC 7816-4.
NFC Type 5 Tag
Метки на основе NFC V.

❈ ❈ ❈

В спецификации для каждого типа меток определяется, как именно устроена память, какие данные в каких ячейках должны лежать и какие команды должны быть реализованы для описанных операций. Например, описанные в предыдущих разделах карты Mifare Ultralight изначально совместимы с NFC Type 2 Tag и для доступа к данным можно пользоваться рассмотренными командами.

А вот карты Mifare Classic изначально не совместимы, однако NXP в отдельном документе (AN305) описывает способ сохранения данных NFC на эти карты. Приложение может реализовать по этому документу поддержку карт Mifare Classic и далее использовать их как обычные NFC-метки.

Также у NXP есть семейство NFC-чипов NTAG, которые разрабатывались специально по спецификациям NFC Forum: NTAG 213, NTAG 215, NTAG 216, NTAG 223, NTAG 424 DNA и другие. Чипы отличаются размером доступной памяти, а также имеют дополнительные возможности типа защиты паролем доступа к данным.

❈ ❈ ❈

Ключевым элементом в модели NFC Forum является формат обмена данными NDEF (сокращение от NFC Data Exchange Format). Главным элементом NDEF является NDEF-сообщение (NDEF message), это бинарный контейнер, состоящий из одной или более NDEF-записей (NDEF record). В NDEF-записи хранятся типизированные и структурированные полезные данные (payload), обычно в одной записи находится один элемент данных. Например, одно NDEF-сообщение может описывать ресторан и состоять из трёх записей: URL сайта ресторана, местоположение (географические координаты в формате геолокационных данных) и текстовое описание. Каждая запись имеет свой стандартный тип, что позволяет сторонним приложениями их корректно интерпретировать.

Работа с пассивными метками (например, NTAG) происходит примерно по такому сценарию:

  • метка (карта) подносится к считывателю;
  • считыватель (и стоящее за ним ПО) определяет тип карты, если данный тип поддерживается, то выбирается алгоритм работы с ним;
  • алгоритм определяет, содержит ли карта NDEF-сообщения, если содержит, то считывает их и передаёт алгоритму обработки NDEF-сообщений;
  • если алгоритм обработки NDEF-сообщений понимает формат и назначение полученных данных, то они передаются дальше для обработки.

Здесь важно отметить, что на каждом из этапов происходит последовательная «распаковка» информации, примерно так, как это происходит при обработке сетевых данных в семиуровневой ISO/OSI модели. Благодаря такому подходу, можно писать изолированные независимые куски кода, которые ничего не знают о семантике данных, с которыми они работают, их цель — распаковать и передать на следующий уровнь обработки.

Например, для карты Mifare Ultralight байты NDEF-сообщения специальным образом распределены среди байтов страниц и задача алгоритма чтения состоит в сборе этих байтов и формировании корректного NDEF-сообщения. При записи аналогично, алгоритм распределяет байты NDEF-сообщения по байтам страниц карты и записывает их. Конкретно карта Mifare Ultralight соответствует спецификации NFC Type 2 Tag, поэтому алгоритм получается простой. А вот для карт Mifare Classic нужно пользоваться уже упомянутым документом AN305 для преобразования, а не спецификацией NFC Forum. При этом на выходе обоих алгоритмов получается одинаковый объект NDEF, который передаётся для дальнейшей обработки. Фактически метки на основе Mifare Classic являются отдельным типом в дополнение к описанным пяти из спецификаций NFC Forum.

❈ ❈ ❈

Но вернёмся к данным. NDEF-сообщение имеет следующую структуру:

NDEF Message structure

Всё сообщение состоит из подряд идущих NDEF записей, каждая из которых начинается с заголовка (record header), в котором задаётся длина, тип и другие параметры записи. Заголовок состоит из следующих полей:

TNF & Flags
Набор битовых флагов и значение TNF:
MB, Message Begin
ME, Message End
Флаги MB и ME определяют положение этой записи внутри всего сообщения: если запись является первой, то флаг MB установлен в 1, если запись является последней, то установлен флаг ME. Если запись одна в сообщении, то установлены оба флага.
CF, Chunk Flag
Этот флаг устанавливается, если полезные данные разбиты на несколько записей, устанавливается в 1 во всех записях, кроме последней. На практике этот флаг почти никогда не используется.
SR, Short Record
Если флаг SR установлен в 1, то длина поля Payload length будет 4 байта, в ином случае — 1 байт.
IL, ID Length
Если флаг IL установлен в 1, то заголовке будет поле ID length, в ином случае этого поля не будет.
TNF, Type Name Format
значение в этих трёх битах определяет структуру поля Payload type и как его интерпретировать:
0x00 — поле Payload type пустое
0x01 — NFC Forum well-known type, NFC record type definition (NFC RTD), в поле Payload type находится одно из значений (в виде строки): T — Text, U — URI, Sp — Smart Poster, ac — Alternative Carrier, Hc — Handover Carrier, Hr — Handover Request, Hs — Handover Select.
0x02 — Media-type, в поле Payload type значение в формате RFC 2046, например, application/octet-stream.
0x03 — Absolute URI, в поле Payload type содержится URI в формате RFC 3986.
0x04 — NFC Forum external type, в поле Payload type записывается URN согласно RFC 2141, но специальным образом упакованный.
0x05 — Unknown, означает, что тип полезных данных неизвестен.
0x06 — Unchanged, это значение используется во втором и последующих кусках chunked-записей (то есть для записей, у которых установлен флаг CF).
0x07 — зарезервировано для будущего использования.
Type length
В этом поле длиной 1 байт записывается длина блока Payload type
Payload length
В этом поле (размером 1 или 4 байта, зависит от флага SR) записывается длина блока с байтами полезных данных.
ID length
В этом поле записывается длина поля Payload ID, наличие этого поля определяется флагом IL.
Payload type
В этом поле записывается тип данных в блоке Payload с полезными данными. Интерпретация записанного здесь значения должна выполняться в соответствии со значением параметра TNF.
Payload ID
В этом поле записывается идентификатор (ID) блока с полезными данными.

Обычно при написании кода вам не нужно вручную разбирать NDEF-сообщения, это гораздо лучше делают соответствующие библиотеки, например, ndeflib.

NDEF-сообщения могут применяться в двух режимах: пассивном, когда с метки считываются или записываются данные; и в peer-to-peer (P2P), когда между двумя NFC-устройствами происходит обмен данными в режиме запроса-ответа. Во втором случае используется Simple NDEF Exchange Protocol (SNEP) вместе с Logical Link Control Protocol (LLCP), но про эти протоколы я в этой статье писать не буду, это большая тема для отдельной статьи.

Теория: NFC Type 2 Tag

На протокольном уровне метки NFC Type 2 Tag частично совместимы с картами Mifare Ultralight, только для использования такой карты в качестве метки её память нужно специальным образом разметить, чтобы программное обеспечение понимало, где искать NDEF-записи и метаданные. Структура памяти следующая (согласно спецификации NFCForum-TS-Type-2-Tag v1.1):

Блокбайт 00байт 01байт 02байт 03комментарии
00Internal0Internal1Internal2Internal3UID/Internal
01Internal4Internal5Internal6Internal7Serial Number
02Internal8Internal9Lock0Lock1Internal / Lock
03CC0CC1CC2CC3Capability Container (CC)
04Data0Data1Data2Data3Data
05Data4Data5Data6Data7Data
...
0FData44Data45Data46Data47Data
...
n....Data
...
.....Lock/Reserved
.....Lock/Reserved
k....Lock/Reserved

Память состоит из блоков, в каждом блоке четыре байта, минимальное количество блоков — 16, с номерами от 0x00 (0) до 0x0F (15). Если блоков больше 16, то под пользовательские данные выделяются блоки с номерами от 4 до k, а блоки с номерами от k+1 до n зарезервированы для дополнительной конфигурации. Зарезервированные блоки могут находиться в любой части памяти, начиная с 16 блока, не обязательно в конце; их положение определяется через специальные конфигурационные байты в блоках с пользовательскими данными. Но обычно они всегда в конце карты, а доступ к ним блокируется на аппаратном уровне метки.

В блоке Capability Container хранится информация о возможностях метки, эти четыре байта являются One-Time Programmable (OTP), то есть в них после записи любого битового поля в 1 оно остаётся навсегда и поменять обратно на 0 невозможно. Для NFC метки структура этих байтов следующая:

CC0CC1CC2CC3
E1PLATFORM VERSIONT2T DATA AREA SIZEACCESS CONDITION

В байте CC0 находится фиксированное значение 0xE1, которое сигнализирует, что это метка NFC Type 2 Tag.

В байте CC1 записывается версия спецификации, в соответствии с которой сделана метка. Для версии 1.0 там записывается 0x10, для версии 1.1 — 11.

В байте CC2 кодируется размер области памяти, выделенной под пользовательские данные. Чтобы получить размер в байтах, нужно значение оттуда умножить на 8. Память под пользовательские данные начинается с блока с номером 0x04.

В байте CC2 кодируются условия доступа к блоку CC и блоку с пользовательскими данными. Старшие четыре бита кодируют доступ на чтение (0 — чтение без ограничений), младшие четыре бита кодируют доступ на запись (0x0 — запись без ограничений, 0xF — запись запрещена). Остальные значения зарезервированы под будущее или проприетарное использование. То есть в метках отдельных производителей там могут использоваться другие значения для обозначения каких-то проприетарных условий доступа.

Вот как выглядят байты CC для метки NTAG215 (135 блоков по 4 байта):

CC0CC1CC2CC3
E1103E00
версия 1.03E=62разрешена запись и чтение

Для пользовательских данных выделено 496 байтов (0x3E = 62, 62 × 8 = 496), это 124 блока, всего на метке 135 блоков, первые четыре зарезервированы, то есть для использования доступны 124 блока, начиная с блока с номером 0x04 и заканчивая блоком с номером 0x81, при этом последние 5 блоков зарезервированы под конфигурацию метки.

В зарезервированных блоках могут находится байты динамической блокировки (dynamic lock bytes), они работают по тому же принципу, что байты Lock0 и Lock1 из блока памяти 02, то есть позволяют навсегда блокировать определённые сегменты памяти и делать их только для чтения.

Также в зарезервированных блоках кодируются другие функциональные возможности, которые описаны в даташитах на конкретный тип метки. Это может быть пароль для блокирования записи, полная блокировка доступа к метке даже на чтение (KILL SWITCH), дополнительные идентификационные данные и так далее.

❈ ❈ ❈

Внутри доступной пользователю области памяти метки информация хранится в виде TLV-объектов, детально о них я расскажу позднее в разделе о микропроцессорных картах, в NFC же используется упрощённый вариант. TLV расшифровывается как Tag-Length-Value и обозначает способ кодирования структур типа ключ-значение в бинарный формат. Ключом служит целое число, а значением — набор байтов. В байтовом представлении сначала идёт один байт для кодирования тега, далее идёт набор байтов для кодирования длины и далее цепочка байтов этой длины.

Длина может кодироваться либо одним байтом, тогда можно закодировать значение от 0x00 до 0xFE включительно. А если значение длины больше 254, то длина кодируется тремя байтами, где первый всегда 0xFF, а два других кодируют значение от 0000 до FFFE включительно.

Такой подход позволяет за один проход декодировать структурированные данные, даже не зная их внутреннюю структуру.

Вот несколько примеров TLV-кодирования объектов.

01 03 AA BB CC

Здесь под тегом 01 закодировано значение длиной 3 байта AA BB CC.

02 FF 01 05 10 20 30 ... 00 00

Здесь под тегом 02 закодировано значение длиной 01 05 (261 байт) 10 20 30 ... 00 00.

03 00

Здесь под тегом 0x03 закодирован байтовый массив нулевой длины, то есть пустой.

При кодировании TLV-объектов в NFC они должны начинаться с начала блока пользовательских данных (с номером 0x04) и располагаться последовательно друг за другом без промежуточных байтов.

❈ ❈ ❈

В спецификации NFC Type 2 Tag определены следующие TLV-объекты:

Название объектаКод тегаОписание
NULL TLV00Может использоваться для разделения и выравнивания TLV-объектов
Lock Control TLV01Определяет позицию байтов с флагами блокировки блоков
Memory Control TLV02Определяет позицию байто с координатами заблокированных блоков
NDEF Message TLV03Содержит NDEF-сообщение в описанном выше формате
Proprietary TLVFDСодержит проприетарную информацию
Terminator TLVFEОбозначает конец сегмента с TLV-объектами

Объекты Lock Control TLV и Memory Control TLV всегда должны располагаться перед объектами NDEF Message TLV и Proprietary TLV.

После объекта Terminator TLV не должно быть никаких других элементов данных метки.

Объекты Terminator TLV и NULL TLV состоят только из одного байта, то есть в них отсутствует блок с длиной и значением.

Если при декодировании данных встречаются объекты с другими тегами, не описанными в спецификации, то они должны игнорироваться.

Способ кодирования данных в спецификации NFC Type 2 Tag весьма сложен в реализации, так как данные могут записываться в память с разрывами в виде сегментов зарезервированных байтов (заданных объектами Lock Control TLV и Memory Control TLV), через которые сегменты реальных данных должны «перепрыгивать» при чтении или записи. Поэтому код для работы с памятью будет довольно сложным, запутанным и местами, возможно, не очень корректным. Но на практике обычно блоки с зарезервированными данными и байтами блокировки располагаются в последних блоках памяти, уже после выделенных для прикладного использования, поэтому цепочка TLV-объектов просто пишется в начало секции последовательно друг за другом и читается аналогично.

❈ ❈ ❈

В примерах ниже я продемонстрирую, как работать с NFC-метками через PC/SC. Поскольку мы не пользуемся прямым доступ к считывателю, то не можем использовать библиотеки типа libnfc или nfcpy, вместо этого все операции будем выполнять исключительно через команды PC/SC. Это сильно ограничивает возможности, но многие операции по-прежнему доступны. Я ограничился только метками NFC Type 2 Tag, так как они наиболее доступные.

example-10: Операции с меткой NFC Type 2 Tag

В этом примере мы рассмотрим чтение и запись NFC меток NFC Type 2 Tag. Подойдут любые метки NTAG 213/215/216, рекомендую купить сразу десяток или больше, пригодится на будущее.

nfc-type2-read

Исходный код программы: example-10/nfc-type2-read

Прежде всего мы должны убедиться, что в считывателе именно NFC-метка нужного типа. Для этого мы читаем блок с номером 3 (Capability container) и проверяем, что его первый байт равен 0xE1:

# read block 0x03 - Capability container
block = 3
#       CLA INS P1  P2      Lc
apdu = 'FF  B0  00  {:02X}  04'.format(block)
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Read Binary failed, probably not NFC Type 2 Tag.')
    return 1

# check capabilities
if response[0] != 0xE1:
    print('No NDEF container capability.')
    print('Capability container bytes:', toHexString(response))
    return 1

Если всё корректно, то считываем всю память метки в один плоский массив блок за блоком:

block = 0
memory = []
while True:
    #       CLA INS P1  P2      Lc
    apdu = 'FF  B0  00  {:02X}  04'.format(block)
    response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
    if (sw1,sw2) != (0x90,0x00):
        break
    memory.extend(response)
    block += 1

Для чтения блоков мы пользуемся стандартной командой Read Binary с кодом инструкции B0, на уровне чтения данных метки этого типа совместимы с картами Mifare Ultralight.

Далее выводим на экран общую информацию о метке (размер памяти, количество блоков и т.п.):

# print spec version, take most significant nibble (major version) and least significant nibble (minor version)
spec_version_byte = memory[3 * BLOCK_SIZE + 1]
print('Spec version: {}.{}'.format(msn(spec_version_byte), lsn(spec_version_byte)))

# print memory info
data_memory_size = memory[3 * BLOCK_SIZE + 2] * 8
print('Total memory: {} bytes in {} blocks'.format(len(memory), len(memory) // BLOCK_SIZE))
print('User data memory size: {} bytes'.format(data_memory_size))
print('User data blocks: {:02X} to {:02X}'.format(4, 3 + data_memory_size // BLOCK_SIZE))
print('Tag configuration blocks: {:02X} to {:02X}'.format(4 + data_memory_size // BLOCK_SIZE, len(memory) // BLOCK_SIZE - 1))

wac_byte = memory[3 * BLOCK_SIZE + 3]  # wac - write access conditions
print('Read access condition: {}'.format(msn(wac_byte)))
print('Write access condition: {}'.format(lsn(wac_byte)))

В глобальной переменной BLOCK_SIZE у нас записан размер блока — 4. Функции lsn и msn возвращают младший и старший полубайты (nibble) соответственно, они определены в конце файла, здесь я их не привожу.

Считаем количество доступной для записи памяти и сохраняем в переменную data_memory_size, заодно считаем и показываем, какой диапазон блоков эта память занимает.

Дальше мы разбираем пользовательскую память на TLV объекты и выводим их на экран:

data_memory = memory[4 * BLOCK_SIZE : 4 * BLOCK_SIZE + data_memory_size]

tlvs = tlvt2t.parse_bytes_list(data_memory)

for t in tlvs:
    if t.tag == 1:
        print('Lock Control TLV:')
        bytes_from, bytes_to = tlvt2t.parse_lock_control_bytes(t.value)
        print('  Bytes from {} to {}: {}'.format(bytes_from, bytes_to, memory[slice(bytes_from, bytes_to)]))
    elif t.tag == 2:
        print('Memory Control TLV:')
        bytes_from, bytes_to = tlvt2t.parse_memory_control_bytes(t.value)
        print('  Bytes from {} to {}: {}'.format(bytes_from, bytes_to, memory[slice(bytes_from, bytes_to)]))
    elif t.tag == 3:
        print('NDEF Message TLV:')
        for record in ndef.message_decoder(bytearray(t.value)):
            print(record)

Для разбора мы пользуемся простым самописным парсером из локального модуля tlvt2t, вот его исходники, он весьма простой и на основе списка байтов создаёт список соответствующих TLV-объектов. Нас интересуют только TLV-объекты с тегом 3, это NDEF-сообщения. Их мы декодируем модулем ndef из библиотеки ndeflib.

Для декодирования объектов Lock Control TLV и Memory Control TLV мы пользуемся соответствующими методами из модуля tlvt2t, их я взял из проекта nfclib (https://github.com/nfcpy/nfcpy/blob/master/src/nfc/tag/tt1.py).

❈ ❈ ❈

Вот как выглядит результат работы программы на пустой чистой метке NTAG 213:

Connected reader: ACS ACR122U PICC Interface
Waiting for card ...
Card connected.
NFC Tag Type 2 detected.
Spec version: 1.0
Total memory: 180 bytes in 45 blocks
User data memory size: 144 bytes
User data blocks: 04 to 27
Tag configuration blocks: 28 to 2C
Read access condition: 0
Write access condition: 0
Lock Control TLV:
  Bytes from 160 to 162: [0, 0]
NDEF Message TLV:

В секции Tag configuration blocks указаны позиции блоков памяти, в которых хранится конфигурация метки, эти байты находятся вне блоков, выделенных для хранения данных.

NDEF-блок пустой, поэтому ничего не выводится. В объекте Lock Control TLV записано местоположение сегмента с байтами флагов контроля блокировок, это байты в позициях 160 и 161. Фактически они находятся за пределами доступной памяти для хранения данных и совпадают с соответствующими байтами из спецификации Mifare Ultralight.

Вы можете воспользоваться программой read-mifare-ultralight из предыдущего примера, чтобы прочитать всю память этой метки и посмотреть, что и где там находится

nfc-type2-write

Исходный код программы: example-10/nfc-type2-write

⭐ Для данного примера обязательно нужен считыватель ACS ACR122U или совместимый с ним, поскольку мы будем пользоваться псевдо-APDU для прямой отправки команд в RFID-модуль PN532. Также вам необходимо взять чистую метку, без сконфигурированных ограничений на запись.

В этой программе мы запишем на метку NDEF-блок, содержащий произвольный URL. Далее эту метку можно будет использовать на смартфоне, чтобы открыть браузер по этой ссылке. Адрес ссылки будем читать из первого аргумента программы.

Сразу скажу, что этот пример очень простой и не учитывает метки, на которых байты с флагами блокировки и адресами зарезервированных сегментов находятся прямо в доступной памяти (теоретически это допустимая ситуация). Мы просто запишем NDEF-блок одним непрерывным фрагментом. Чистая метка типа NTAG 213 или NTAG 215 идеально для этого подойдёт.

Согласно спецификации NFCForum-TS-Type-2-Tag, сегменты памяти с NDEF-сообщениями должны идти строго после сегментов с Lock Control TLV (тег 1) и Memory Control TLV (тег 2), поэтому мы сначала прочитаем все TLV-объекты с метки, возьмём первые объекты с тегами 1 и 2 и добавим к ним сгенерированный TLV-объект с NDEF-сообщением, а завершим всё Terminator TLV. Далее сконвертируем список TLV-объектов в список байтов, дополним нулями до размера, кратного размеру блока (4) и запишем этот список байтов в память.

❈ ❈ ❈

Считыватель ACS ACR122U предоставляет специальный APDU-запрос Direct Transmit для отправки команд непосредственно в радио-модуль PN532 (полный список команд и другую информацию можно посмотреть в инструкции к модулю).

Формат запроса Direct Transmit следующий:

CLAINSP1P2LcData
FF000000длина PayloadPayload

Отправляемая команда кодируется в поле Payload следующим образом (согласно спецификации из инструкции к PN532):

D4 Command Byte Command Data
  • 0xD4 — фиксированное значение, обозначает фрейм данных, отправляемый в модуль
  • Command Byte — код (один байт) команды
  • Command Data — данные в специфичном для команды формате

Если модуль обработал команду и вернул результат, то ответ возвращается со статусным словом 90 00 и блоком байтов следующего формата:

D5 Command Check Byte Response Data
  • 0xD5 — фиксированное значение, обозначает фрейм данных, отправляемый из модуля
  • Command Check Byte — здесь возвращается значение Command Byte из запроса плюс единица
  • Response Data — данные ответа в специфичном для конкретной команды формате, здесь кодируется в том числе статус, успешно ли завершилась команда, а если успешно, то возвращаются байты ответа

⭐ На самом деле формат запросов и ответов к модулю несколько другой и содержит ещё ряд полей, однако считыватель заполняет их за нас и удаляет лишнее при возврате ответа.

В этом примере мы будем пользоваться только APDU Direct Transmit для всех операций, в них инкапсулируются последовательно: команда считывателя → команда радио-модуля → NFC-команда метки.

FF 00 00 00 09 D4 40 01 A2 04 03 10 D1 01

команда считывателя (APDU)
|
CLA INS P1 P2  Lc  команда радио-модуля
|                  |          NFC-команда метки для записи байтов `03 10 D1 01` в блок `04`
|                  |          |
FF  00  00 00  09  D4  40 01  A2 04 03 10 D1 01

❈ ❈ ❈

Все APDU запросов в радио-модуль мы будем формировать через простую функцию pn532_c_apdu:

def pn532_c_apdu(cmd):
    cmd_bytes = toBytes(cmd)
    return 'FF 00 00 00 {:02X} D4 {}'.format(len(cmd_bytes) + 1, toHexString(cmd_bytes))

На вход она получает команду для модуля и возвращает полный псевдо-APDU для считывателя. Для преобразования корректного ответа от модуля мы используем другую функцию pn532_r_apdu, которая отрезает первые два байта с кодом фрейма D5 и байтом контроля кода инструкции:

def pn532_r_apdu(b):
    return b[2:]

В результате остаётся байтовый массив, первый элемент которого почти для всех команд содержит статус выполнения команды.

Команда для коммуникации с радио-модулем (далее команда модуля) называется InDataExchange и описана в разделе 7.3.8 инструкции к модулю) PN532. Код команды состоит из байта 0x40 и двух аргументов, первый из которых в нашем случае всегда 0x01, а второй — собственно команда для коммуникации с меткой (они описаны в спецификациях NFC и даташитах конкретных меток, я их дальше буду называть командами меток).

Для чтения памяти мы будем использовать команду метки READ с кодом 0x30 из спецификации NFCForum-TS-Type-2-Tag, , команда выглядит как 30 XX, где XX — номер блока, начиная с которого нужно прочитать 16 байтов (то есть каждый такой запрос читает сразу четыре блока).

Для начала мы попытаемся прочитать первые 16 байтов метки, чтобы выделить из байтов Capability Container общую информацию о метке, в первую очередь — размер памяти. Память записываем в переменную-список memory.

apdu = pn532_c_apdu('40 01 30 {:02X}'.format(block))
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Read Binary failed, probably not NFC Type 2 Tag.')
    return 1
response = pn532_r_apdu(response)
memory.extend(response[1:])
if response[0] != 0:
    print('Failed to read tag header.')
    return 1

Здесь мы проверяем статус выполнения команды, если первый байт response[0] не равен нулю, значит, что-то пошло не так и дальше работу продолжать нельзя.

Когда первые 16 байтов прочитаны, мы можем из них выделить размер памяти и общее количество блоков для чтения. Здесь такой же код, как и в прошлом примере:

data_memory_size = memory[3 * BLOCK_SIZE + 2] * 8
data_memory = memory[4 * BLOCK_SIZE : 4 * BLOCK_SIZE + data_memory_size]
total_blocks = data_memory_size // 4

Дальше мы читаем оставшуюся память такими же запросами по 16 байтов:

while True:
    if block >= total_blocks:
        break
    # read segments of memory using PN532-command InDataExchange (code 40 01) and NFC-command READ (code 30)
    apdu = pn532_c_apdu('40 01 30 {:02X}'.format(block))
    response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
    if (sw1,sw2) != (0x90,0x00):
        print('Read Binary failed, probably not NFC Type 2 Tag.')
        return 1
    response = pn532_r_apdu(response)
    if response[0] != 0:
        break
    memory.extend(response[1:])
    block += 4

Дальше обязательно парсим память на TLV-блоки, поскольку нам нужно выделить объекты Lock Control TLV и Memory Control TLV, которые должны располагаться перед сегментами памяти с NDEF-сообщениями. Эти объекты выделяем и сразу записываем в итоговый список write_tlvs:

tlvs = tlvt2t.parse_bytes_list(data_memory)
write_tlvs = []

for t in tlvs:
    if t.tag != 1 and t.tag != 2:
        break
    write_tlvs.append(t)

Далее генерируем NDEF-запись на основе аргумента с URL, из неё NDEF-сообщение (пользуемся библиотекой ndeflib), из него TLV объект, который записываем в список write_tlvs, а завершаем всё терминирующим TLV-объектом:

url = sys.argv[1]
record = ndef.UriRecord(url)
message = [record]
ndef_bytes = b''.join(ndef.message_encoder(message))
write_tlvs.append(tlvt2t.TLV(0x03, list(ndef_bytes)))
write_tlvs.append(tlvt2t.TLV(0xFE, []))
tlv_data = tlvt2t.pack_tlv_list(write_tlvs)

В переменной tlv_data находится бинарное представление всего блока TLV-объектов и мы можем его теперь записать на метку. Так как запись происходит блоками, мы сначала выравниваем длину до значения, кратного 4:

pad_size = (BLOCK_SIZE - len(tlv_data) % BLOCK_SIZE) % BLOCK_SIZE
tlv_data.extend([0] * pad_size)

Для записи используем команду метки WRITE с кодом 0xA2, команда имеет два аргумента: номер блока и четыре байта содержимого блока для записи. Данные пишем, начиная с блока с индексом 4. Если статус выполнения команды метки не равен нулю, выводим сообщение об ошибке, но продолжаем попытки записи следующих блоков.

for i in range(len(tlv_data) // BLOCK_SIZE):
    chunk = tlv_data[i * BLOCK_SIZE : (i + 1) * BLOCK_SIZE]
    block = 4 + i
    apdu = pn532_c_apdu('40 01  A2 {:02X} {}'.format(block, toHexString(chunk)))
    response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
    if (sw1,sw2) != (0x90,0x00):
        print(f'Failed to update block {block}!')
        return 1
    response = pn532_r_apdu(response)
    if response[0] != 0:
        print('Failed to update block {:02X}'.format(block))

❈ ❈ ❈

Вызывается программа так:

% ./nfc-type2-write https://blog.regolit.com
Connected reader: ACS ACR122U PICC Interface
Waiting for card ...
Card connected.
Reading tag ... done
Writing tag ... done

После успешной записи вы можете протестировать метку на телефоне с возможностью чтения NFC, при поднесении его к метке должен открыться браузер с тем адресом, который вы записали.

❈ ❈ ❈

Производители NFC меток обычно добавляют в свои устройств дополнительные функции помимо тех, которые описаны в спецификациях к меткам. Чаще всего это защита паролем доступа (на запись или чтение/запись) к определённой области памяти.

Спецификацией NFCForum-TS-Type-2-Tag предусмотрена только перманентная блокировка отдельных регионов памяти от записи, можно вообще полностью заблокировать метку от записи, сделав её только для чтения.

Работа с контактными картами памяти

Контактные (то есть с контактной площадкой) карты памяти не имеют внутренней программной логики (как и карты Mifare Classic) и требуют для работы считыватель, понимающий их сигнальный протокол. Они по сути даже не являются «умными» (smart) картами и не поддерживают APDU. Такие карты очень дешёвые и когда-то использовались в качестве средств оплаты в таксофонах, например. А сейчас иногда продолжают использоваться в системах контроля доступа гостиниц. Cодержимое карты памяти обычно свободно доступно для чтения, а для записи требуется предварительная аутентификация через секретный PIN. Также карты могут снабжаться защитой от перебора PIN: после небольшого количества неудачных попыток карта блокируется.

⭐ К настоящему времени контактные карты памяти практически вышли из употребления и мало где используются. Однако на их примере очень удобно изучать базовые принципы работы с картами, считывателями, спецификациями и документацией.

Самым популярным чипом для контактных карт памяти является семейство SLE, изначально разработанное фирмой Siemens (позднее подразделение выделилось в отдельную фирму Infineon Technologies AG). Вот несколько популярных чипов этого семейства:

  • SLE 4418 — 1 KByte EEPROM with Write Protect Function
  • SLE 4428 — 1 KByte EEPROM with Write Protect Function and Programmable Security Code (PSC)
  • SLE 5518 — 1 KByte EEPROM with Write Protect Function (замена SLE 4418)
  • SLE 5528 — 1 KByte EEPROM with Write Protect Function and Programmable Security Code (PSC) (замена SLE 4428)
  • SLE 4432 — 256-Byte EEPROM with Write Protect Function
  • SLE 4442 — 256-Byte EEPROM with Write Protect Functionand Programmable Security Code (PSC)
  • SLE 5532 — 256-Byte with Write Protection (замена SLE 4432)
  • SLE 5542 — 256-Byte with Write Protection and Programmable Security Code (PSC) (замена SLE 4442)
  • SLE 4440 — 64-Byte EEPROM with Write Protect FunctionSLE 4440and Programmable Security Code (PSC)
  • SLE 4441 — 128-Byte EEPROM with Write Protect FunctionSLE 4441and Programmable Security Code (PSC)

Другой популярный производитель чипов — Atmel, некоторые его чипы:

  • AT24C02C — 2048 bit EEPROM
  • AT24C128C — 128 kbit EEPROM
  • AT88SC153 — 2048 bit EEPROM (64 bytes configuration zone, 192 bytes user data)
  • AT88SC1608 — 17408 bit EEPROM (128 byte configuration zone, 2048 bytes bytes user data)

❈ ❈ ❈

Технологически контактные карты памяти соответствуют стандарту ISO/IEC 7816-10: Electronic signals and answer to reset for synchronous cards, он определяет сигнальный (электрический) протокол для синхронных карт, при этом карты НЕ соответствуют стандарту ISO/IEC 7816-4, который описывает протоколы для обмена данными. Другими словами, чтобы взаимодействовать с подобной картой, вы должны вручную отправлять и считывать электрические сигналы определённой формы на определённых контактах чипа. Формат сигналов и закодированные в них команды описываются в даташитах каждого чипа, например, в SLE 4432 datasheet.

Считыватели могут, но не обязаны поддерживать разные типы карт. Это делается через псевдо-APDU в классе CLA=FF (примерно как в случае с картами Mifare Classic), и эти APDU разные для разных считывателей и карт. Например, в руководстве к считывателю ACR38U-I1 такие команды описаны для каждой из поддерживаемых карт в разделе 9.3. Memory Card Command Set. Чтобы работать с картами памяти, вам нужно одновременно смотреть в руководство к считывателю и в руководство к конкретному типу карты. Универсальных команд для всех считывателей не существует.

example-11: Работа с контактной картой памяти SLE 5542

Для этого примера можно использовать только считыватель ACR38U-I1 (или другой этого же производителя с той же системой команд) и карту памяти SLE5542/SLE4442.

Цели этого примера:

  • показать, как читать и писать на контактную карту памяти через считыватель;
  • показать, как правильно читать спецификации карты и считывателя.

Прежде всего понадобятся два документа, с которыми вы будете постоянно сверяться:

  • SLE 4442 datasheet (даташит для SLE 5542 ссылается на 4442, поэтому ссылку даю именно на 4442), дальше ссылаюсь на него как [SLE]
  • ACR38U-I1 reference manual, дальше ссылаюсь на него как [ACR]

Нужные нам псевдо-APDU для карты SLE 5542 описаны в разделе 9.3.6. Memory Card – SLE 4432/SLE 4442/SLE 5532/SLE 5542 ([ACR], стр.47).

В первую очередь мы должны понять, какие данные и как хранятся на карте. В [SLE] на странице 7 описаны характеристики чипа, нас интересуют вот такие:

  • 256×8-bit EEPROM organization — то есть на карте 256 байтов памяти в EEPROM;
  • Byte-wise addressing — побайтовая адресация;
  • Irreversible byte-wise write protection of lowest 32 addresses (Byte 0 ... 31) — первые 32 байта в EEPROM (с адресами от 0 до 31) можно защитить от перезаписи;
  • 32×1-bit organization of protection memory — 32 бита (то есть 4 байта), которые используются в предыдущем пункте для контроля защиты от перезаписи (эти биты хранятся отдельно от 256 байтов основной памяти и в терминологии [SLE] называются Protection memory);
  • Data can only be changed after entry of the correct 3-byte programmable security code (security memory) — для записи данных нужна предварительная аутентификация трёхбайтовым кодом (эти байты хранятся отдельно от 256 байтов основной памяти и в терминологии [SLE] называются Security memory).

Итак, читать данные с карты можно всегда, никакой защиты для этого не предусмотрено. Из 256 байтов первые 32 можно аппаратно защитить от повторной перезаписи (навсегда, снять защиту невозможно). А в целом для любой записи нужна аутентификация трёхбайтовым кодом (пином). Защита пином — это особенность чипов SLE 5542 и SLE 4442, в чипах SLE 4432 и SLE 5532 такой защиты нет.

Структура первых 32 байтов EEPROM карт SLE4442/SLE5542 фиксированна и подробно описана в [SLE], в разделе 2.8 Coding of the Chip. Часть из этих полей защищена от перезаписи производителем чипа.

sle-card-read

Исходный код программы: example-11/sle-card-read

В этой программе мы прочитаем содержимое всех 256 байтов EEPROM, содержимое Protection memory и счётчик ошибок авторизации.

Для начала мы должны сообщить считывателю, какую именно карту мы хотим использовать. Это делает инструкция, описанная в [ACR] в разделе 9.3.6.1. SELECT_CARD_TYPE:

#       CLA INS P1  P2  Lc  DATA
apdu = 'FF  A4  00  00  01  06'
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Select failed')
    return 1

Эта инструкция ОБЯЗАТЕЛЬНА при работе с картой, если её не выполнить, результат будет непредсказуемым.

❈ ❈ ❈

Псевдо-APDU для чтения памяти описан [ACR], в разделе 9.3.6.2. READ_MEMORY_CARD. Но с ним есть одна небольшая проблема — им можно прочитать максимум 255 байтов, так как в одном байте можно указать максимум 0xFF, то есть 255. Поэтому прочитать все 256 байтов мы сможем минимум в два прохода: сначала первые 32, а затем оставшиеся 224, после чего сольём два массива в один:

# Instruction "9.3.6.2. READ_MEMORY_CARD"
# Read all 256 bytes in two passes.
# First read 32=0x20 (in "Le" field) bytes starting with address 0x00 (in "P2" field)
# field "P1" is ignored

#       CLA INS P1  P2  Le
apdu = 'FF  B0  00  00  20'
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Cannot read card data')
    return 1
eeprom_data = response

# Then read remaining 224=0xE0 (in "Le" field) bytes starting with address 0x20 (in "P2" field)
# field "P1" is ignored

#       CLA INS P1  P2  Le
apdu = 'FF  B0  00  20  E0'
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Cannot read card data')
    return 1
eeprom_data.extend(response)

Распечатаем все байты блоками по 32:

print('EEPROM memory:')
for i in range(0, len(eeprom_data), 32):
    chunk = eeprom_data[i:i+32]
    print(' ', toHexString(chunk))

❈ ❈ ❈

Чтение содержимого Protection memory через псевдо-APDU из раздела 9.3.6.4. READ_PROTECTION_BITS:

# Instruction "9.3.6.4. READ_PROTECTION_BITS"
# read 0x04 (in "Le" field) bytes of Protection memory
# fields "P1" and "P2" are ignored

#       CLA INS P1  P2  Le
apdu = 'FF  B2  00  00  04'
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Cannot read card data')
    return 1
prb_data = response

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

Результат PRBData состоит из четырёх байтов, обозначим их как PRB1, PRB2, PRB3, PRB4. Согласно [ACR] (стр. 50), биты в этих байтах отображаются на первые тридцать два байта EEPROM следующим образом:

      PRB1             │      PRB2                    │      PRB3                     │      PRB4
═══════════════════════╪══════════════════════════════╪═══════════════════════════════╪═══════════════════════════════ 
P8 P7 P6 P5 P4 P3 P2 P1│P16 P15 P14 P13 P12 P11 P10 P9│P24 P23 P22 P21 P20 P19 P18 P17│P32 P31 P30 P29 P28 P27 P26 P25   

Поэтому получаем такой код (0 означает, что байт нельзя перезаписывать, 1 — можно):

print('Protection memory bits:')
print('  ', end='')
for x in range(32):
    print('{:02X} '.format(x), end='')
print('')
protection_bits = [0 for x in range(32)]
for k in range(4):
    b = prb_data[k]
    for i in range(8):
        addr = k * 8 + i
        protection_bits[addr] = b & 1
        b >>= 1
print('  ', end='')
for x in range(32):
    print('{: 2X} '.format(protection_bits[x]), end='')
print('')

❈ ❈ ❈

Также распечатаем значение счётчика ошибок, эта команда описана в разделе 9.3.6.3. READ_PRESENTATION_ERROR_COUNTER_MEMORY_CARD (SLE 4442 and SLE 5542):

# Instruction "9.3.6.3. READ_PRESENTATION_ERROR_COUNTER_MEMORY_CARD (SLE 4442 and SLE 5542)"
# read 0x04 (in "Le" field) bytes of Security memory (only EC value is returned)
# fields "P1" and "P2" are ignored

#       CLA INS P1  P2  Le
apdu = 'FF  B1  00  00  04'
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Cannot read card data')
    return 1
ec_data = response

print('EC: {:02X}'.format(ec_data[0]))

❈ ❈ ❈

Вот как выглядит запуск этой программы у меня на новой чистой карте:

% ./sle-card-read
Connected reader: ACS ACR 38U-CCID
Waiting for card ...
Card connected.
EEPROM memory:
  A2 13 10 91 FF FF 81 15 FF FF FF FF FF FF FF FF FF FF FF FF FF D2 76 00 00 04 00 FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
Protection memory bits:
  00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
   0  0  0  0  1  1  0  0  1  1  1  1  1  1  1  1  1  1  1  1  1  0  0  0  0  0  0  1  1  1  1  1
EC: 07

Как видно, первые четыре байта защищены от перезаписи. В них хранятся данные из ATR. Другой защищённый фрагмент — 81 15 — содержит ICM=81 (IC Manufacturer Identifier, идентификатор производителя чипа) и ICT=15 (IC Type, тип карты).

sle-card-write

Исходный код программы: example-11/sle-card-write

В этой программе мы запишем что-нибудь на карту.

Сначала выбираем карту инструкцией SELECT_CARD_TYPE точно так же, как и в предыдущей программе.

Далее всего нам нужно авторизоваться с помощью трёхбайтового кода (Programmable Security Code, PSC), на новой карте PSC в hex-виде выглядит так: FF FF FF. Каждая неудачная попытка авторизации увеличивает счётчик ошибок (Error Counter, EC) и после трёх ошибок подряд карта блокирует навсегда все попытки авторизоваться и, соответственно, возможность что-либо записать на карту. После успешной авторизации счётчик сбрасывается.

Авторизация через PSC описана в [ACR] в разделе 9.3.6.7. PRESENT_CODE_MEMORY_CARD (SLE 4442 and SLE 5542) (стр.53):

# Instruction "9.3.6.7. PRESENT_CODE_MEMORY_CARD (SLE 4442 and SLE 5542)"
# write 3 bytes of PSC
# field "P1" is ignored

#       CLA INS P1  P2  Lc  DATA
apdu = 'FF  20  00  00  03  FF FF FF'
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x07):
    print('PSC auth failed')
    return 1

Важный момент: мы сравниваем статусное слово (SW) не с обычным значением 0x9000, а с 0x9007! Именно такой код успешного выполнения этой команды.

Дальше записываем данные, четыре байта 01 02 03 04, начиная с адреса 64=0x40:

# Instruction "9.3.6.5. WRITE_MEMORY_CARD"
# write 4 bytes "01 02 03 04" starting with address 0x40 (in "P2" field)
# field "P1" is ignored

print('Writing data ... ', end='')
#       CLA INS P1  P2  Lc  DATA
apdu = 'FF  D0  00  40  04  01 02 03 04'
response, sw1, sw2 = cardservice.connection.transmit(toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Write memory failed')
    return 1
print('done')

Если мы теперь прочитаем карту предыдущей программой sle-card-read, то увидим записанные нами байты:

% ./sle-card-read
Connected reader: ACS ACR 38U-CCID
Waiting for card ...
Card connected.
EEPROM memory:
  A2 13 10 91 FF FF 81 15 FF FF FF FF FF FF FF FF FF FF FF FF FF D2 76 00 00 04 00 FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  01 02 03 04 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
Protection memory bits:
  00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
   0  0  0  0  1  1  0  0  1  1  1  1  1  1  1  1  1  1  1  1  1  0  0  0  0  0  0  1  1  1  1  1
EC: 07

❈ ❈ ❈

На этом месте я пример завершаю. За кадром остались такие инструкции:

  • 9.3.6.6. WRITE_PROTECTION_MEMORY_CARD — защита определённых байтов изменением Protection memory;
  • 9.3.6.8. CHANGE_CODE_MEMORY_CARD (SLE 4442 and SLE 5542) — изменение пин-кода PSC.

Вы можете с ними поэкспериментировать самостоятельно.

Теория: отраслевые стандарты

Ранее я уже неоднократно упоминал международные технические стандарты, которым должны соответствовать смарт-карты. Сейчас я об этом расскажу подробнее.

Самым главным стандартом является семейство ISO/IEC 7816, оно состоит из более чем десяти частей, описывающих разные аспекты классических смарт-карт. Часть этих стандартов официально переведена на русский язык и выпущена в виде ГОСТов (в этом случае я даю ссылки на эти переводы).

Пять стандартов определяют физические и электрические характеристики карт/интерфейсов:

ISO/IEC 7816-1: Cards with contacts — Physical characteristics

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-1: Карты с контактами. Физические характеристики.

Определяет физические характеристики карт с контактами: размер, требования к прочности и механической надёжности.

ISO/IEC 7816-2: Cards with contacts — Dimensions and location of the contacts

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-2: Карты с контактами. Размеры и расположение контактов

Определяет размеры, расположение и обозначение внешних проводящих контактов на карте.

ISO/IEC 7816-3: Cards with contacts — Electrical interface and transmission protocols

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-3: Карты с контактами. Электрический интерфейс и протоколы передачи

Определяет электрический интерфейс и протоколы передачи для асинхронных карт

ISO/IEC 7816-10: Electronic signals and answer to reset for synchronous cards

Официального перевода на русский нет.

Определяет электрический интерфейс и ответ-на-восстановление (answer to reset, ATR) для синхронных карт.

ISO/IEC 7816-12: Cards with contacts — USB electrical interface and operating procedures

Официального перевода на русский нет.

Определяет электрический интерфейс и рабочие процедуры для USB карт.

❈ ❈ ❈

Остальные стандарты этого семейства не зависят от конкретной технологии физического интерфейса:

ISO/IEC 7816-4: Organization, security and commands for interchange

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-4: Карты идентификационные. Карты на интегральных схемах. Часть 4. Организация, защита и команды для обмена.

Определяет организацию, защиту и команды для обмена информацией. Это самый главный стандарт для отрасли, именно он является базой для всех остальных стандартов.

ISO/IEC 7816-5: Registration of application providers

Официального перевода на русский нет.

Определяет, как использовать идентификаторы приложений, а также задаёт нормативную базу для центральной регистрации идентификаторов.

ISO/IEC 7816-6: Interindustry data elements for interchange

Официальный перевод: ГОСТ Р ИСО/МЭК 7816-6: Карты идентификационные. Карты на интегральных схемах. Часть 6. Межотраслевые элементы данных для обмена

Задаёт элементы данных, которые используются при межотраслевом обмене данными, основанном на смарт-картах. Для каждого элемента задаёт название, идентификатор, формат, кодирование, ссылку на стандарт данных.

ISO/IEC 7816-7: Interindustry commands for Structured Card Query Language (SCQL)

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-7: Карты идентификационные. Карты на интегральных схемах с контактами. Часть 7. Межотраслевые команды языка структурированных запросов для карт (SCQL)

Определяет команды языка структурированных запросов для карт

ISO/IEC 7816-8: Commands and mechanisms for security operations

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-8: Карты идентификационные. Карты на интегральных схемах. Часть 8. Команды для операций по защите информации.

Определяет межотраслевые команды для карты, которые могут быть использованы для операций криптографии.

ISO/IEC 7816-9: Commands for card management

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-9: Карты идентификационные. Карты на интегральных схемах. Часть 9. Команды для управления картами.

Определяет межотраслевые команды для управления картой и файлами. Эти команды охватывают полный жизненный цикл карты, и поэтому некоторые команды могут использоваться до того, как карта будет выдана держателю карты или после того, как истечет срок действия карты.

ISO/IEC 7816-11 Personal verification through biometric methods

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-11: Карты идентификационные. Карты на интегральных схемах. Часть 11. Верификация личности биометрическими методами.

Устанавливает межотраслевые команды, связанные с системой защиты и используемые для верификации личности биометрическими методами в картах на интегральных схемах. Также определяет структуру данных и методы доступа к данным для использования карты в качестве носителя биометрических эталонных данных и/или в качестве устройства, позволяющего выполнить верификацию личности биометрическими методами (т.е. «он-карт» сопоставление).

ISO/IEC 7816-13: Commands for application management in multi-application environment

Официальный русский перевод: ГОСТ Р ИСО/МЭК 7816-13: Карты идентификационные. Карты на интегральных схемах. Часть 13. Команды для управления приложениями в мульти-прикладной среде.

Определяет команды для управления приложениями в мульти-прикладной (то есть на карте установлено несколько разных приложений) среде. Данные команды охватывают полный жизненный цикл приложений в мульти-прикладной карте на интегральной схеме; команды можно использовать до и после того, как карта будет выдана держателю карты.

ISO/IEC 7816-15: Cryptographic information application

Официального перевода на русский нет.

Определяет приложение для работы с криптографическими данными: хранение, получение, обработка, аутентификация.

❈ ❈ ❈

Стандарт ISO/IEC 14443: Identification cards — Contactless integrated circuit cards — Proximity cards определяет физические характеристики, частоты, сигнальный интерфейс, протокол передачи данных для бесконтактных карт. Он состоит из четырёх частей и все они официально переведены на русский язык.

❈ ❈ ❈

Near Field Communication — NFC. Эта технология базируется на ISO/IEC 14443 и ISO/IEC 18000-3. ISO/IEC 18000 — это стандарт для RFID-меток, а его часть 3 описывает технические параметры радиоинтерфейса на частоте 13,56 МГц. Для координации усилий по развитию NFC был создан консорциум NFC Forum, о нём я писал выше. Также есть другие организации, развивающие собственные аспекты этой технологии.

Радио-аспекты NFC описываются следующими стандартами:

  • ISO/IEC 18092 / ECMA-340—Near Field Communication Interface and Protocol-1 (NFCIP-1)
  • ISO/IEC 21481 / ECMA-352—Near Field Communication Interface and Protocol-2 (NFCIP-2)

❈ ❈ ❈

Банковские карты используют стандарт EMV, о нём я подробно поговорю в следующих разделах.

❈ ❈ ❈

В GSM-телефонии для SIM-карт используются технические стандарты European Telecommunications Standards Institute, в частности ETSI TS 102 221: Smart Cards; UICC-Terminal interface; Physical and logical characteristics.

❈ ❈ ❈

Все эти стандарты связаны между собой и часто ссылаются друг на друга. Если вы собираетесь вдумчиво изучать область смарт-карт, то придётся эти стандарты доставать и внимательно изучать. Также имейте в виду, что каждая конкретная карта поддерживает лишь какую-то часть стандарта, а не весь его целиком.

Теория: взаимодействие с микропроцессорной картой

Карты Mifare Classic или SLE5542 — это карты памяти, то есть они не предоставляют никакой дополнительной логики, кроме чтения и записи байтов. В этом разделе речь пойдёт о других — полноценных микропроцессорных картах с операционной системой и приложениями.

Примеры микропроцессорных карт:

  • банковские пластиковые карты с чипом;
  • SIM-карты для мобильных телефонов;
  • электронные удостоверения и паспорта;
  • криптографические USB-токены (например, yubikey или Rutoken).

Все эти карты сделаны и функционируют в соответствии со стандартами семейства ISO/IEC 7816, в частности, позволяют использовать описанные в ISO/IEC 7816-4 APDU для коммуникации.

Главным считывателем для этого и последующих разделов будет считыватель карт с контактной площадкой. Соответственно нам понадобятся для экспериментов разнообразные чиповые карты: кредитки, сим-карты и так далее.

⭐ Также обратите внимание, что когда я ссылаюсь на конкретные разделы из текста стандарта ISO/IEC 7816, я имею в виду редакцию 2020 года. В предыдущих редакциях (2005 и 2013) нумерация и структура текста была другой.

Работа с данными

К сожалению, не существует универсального метода для доступа к данными на смарт-карте. Карты разных типов требуют разных подходов. Самые распространённые на сегодняшний день типы микропроцессорных смарт-карт: чиповые банковские пластиковые карты и SIM-карты для GSM-телефонов.

Главный технический стандарт для банковских карт называется EMV, по первым буквам Europay, MasterCard, Visa. Документация по нему доступна для свободного скачивания с официального сайта, на момент написания статьи последняя версия EMV 4.3 и скачать файлы можно отсюда: https://www.emvco.com/emv-technologies/contact/. Работе с EMV-картами будет дальше посвящён один большой пример.

Современное название смарт-карт для GSM/UMTS — UICC (Universal Integrated Circuit Card), изначально назывались SIM-картами, но позднее функциональность существенно расширилась. Развитием стандарта занимается ETSI (European Telecommunications Standards Institute), документы свободно доступны с официального сайта организации: https://www.etsi.org

Операционная система на карте

На микропроцессорной карте установлена собственная операционная система. Именно она определяет поведение карты. Физически и электрически карта может соответствовать стандартам ISO/IEC 7816-1 и ISO/IEC 7816-2, однако может реализовывать ISO/IEC 7816-3 или ISO/IEC 7816-4 только частично.

Такую операционную систему называют COS (Chip Operating System). Она состоит из двух частей: базовая часть (реализует основные низкоуровневые операции) и специфичная для предметной области (реализует конкретную функциональность, например, криптографию). Впрочем, такое деление весьма условное, изначально карты могли выполнять только одну функцию. Позднее появились карты, на которых параллельно может работать несколько независимых «приложений», реализующих совершенно разные функции.

На заре технологий каждый производитель карт делал собственную проприетарную операционную систему. Приложения для каждой из таких систем приходилось писать отдельно и они обычно шли с завода уже установленными на карту. С развитием технологий возникла потребность в COS общего назначения: чтобы бизнес мог самостоятельно писать приложения и загружать их на карту. Так появились Java Card OS (от Sun Microsystems) и Multos (вышедшая из банковско-финансового сообщества). На текущий момент это два главных игрока на рынке COS.

Кроме того, была образована организация GlobalPlartform (http://www.globalplatform.org/), занимающаяся стандартизацией разнообразных задач, связанных со смарткартами и приложениями на них.

Поверх COS работает собственно приложение (application), например, банковское. ISO/IEC 7816 определяет приложение следующим образом: структуры, элементы данных и программные модули, необходимые для выполнения определенных функций. По сути это набор файлов и программ, объединённых общей задачей.

Данные: файлы и приложения

Коммуникация программы с картой происходит через считыватель: программа отправляет команды (C-APDU) и получает ответы (R-APDU). Целиком их формат описывается в разделе 5 Command-response pairs стандарта ISO/IEC 7816-4. Коротко я про него уже рассказывал в разделе Структура пары команда-ответ: это набор байтов, состоящий из четырёх обязательных байтов заголовка (CLA, INS, P1, P2), и нескольких опциональных (Le, Lc, DATA), при этом все виды C-APDU можно разбить на 4 варианта: Вариант 1, Вариант 2, Вариант 3, Вариант 4.

Старший бит b8 в CLA определяет принадлежность класса. Если b8=0 (то есть значения от 0x00 до 0x7F включительно), то это межотраслевой (interindustry) класс и его дальнейшая структура описывается в стандарте ISO/IEC 7816. Если b8=1 (то есть значения от 0x80 до 0xFF включительно), то это проприетарный (proprietary) класс и его структура может описываться где-то ещё, в других стандартах или спецификациях. Для межотраслевого класса остальные биты в байте CLA задают разные функции (например, логический канал или шифрование данных в команде), которые детально описаны в стандарте. По умолчанию часто байт CLA выставляется в 0x00, а если там предполагается какое-то иное значение, это будет специально указано.

Каждая отрасль определяет собственные команды и форматы данных, обычно для таких инструкций используется проприетарный класс команд CLA. Например, для банковских карт используется CLA=80, а для SIM/UICC — CLA=A0. А CLA=FF активно используется в считывателях для формирования запросов, которые преобразуются считывателем в команды для конкретных карт.

❈ ❈ ❈

В общем случае взаимодействие внешней программы с картой через считыватель можно представить как диалог из C-APDU (команда, APDU-команда) и R-APDU (ответ, APDU-ответ). Микропроцессор карты обрабатывает каждую команду и генерирует ответ. Некоторые команды описаны в ISO/IEC 7816-4 и вызывающая их программа ожидает ответ определённой структуры. Другие команды описаны в отраслевых стандартах со своей семантикой. Очень условно все команды можно разделить на две группы:

  • команды для работы с записанными на карту статическими данными: чтение, запись, удаление, поиск и так далее;
  • команды для выполнения операций на карте, например, генерация закрытого ключа в крипто-токене или генерация цифровой подписи для переданной строки байтов.

Команды могут менять состояние карты. Например, для чтения определённого блока данных нужно последовательно отправить несколько APDU. Порядок выполнения команд также имеет значение, нельзя запускать произвольную команду в произвольное время.

❈ ❈ ❈

Стандарт ISO/IEC 7816-4 определяет специальную логическую модель для доступа к данным на карте. Они представлены объектами, которые в терминологии стандарта называются файлами (files). Файл в этой модели отличается от файла в десктопной операционной системе, то есть это не массив произвольных байтов, который можно прочитать или записать, а объект с определёнными свойствами, доступ к которым осуществляется через APDU-команды. К этому объекту можно обратиться разными способами, которые выбираются производителями карты на основе полного списка из стандартов. Например, через идентификатор или имя, я ниже расскажу подробно, что это такое.

Файлы (точнее, как они видятся через считыватель) могут образовывать иерархическую структуру. Для этого ISO/IEC 7816-4 определяет такие их типы:

elementary file (элементарный файл) / EF

Объект с данными, не может содержать в себе других объектов. Определены два типа EF:

internal elementary file (внутренний элементарный файл) / IEF
данные в нём читает и пишет исключительно операционная система карты или карточные приложения. Например, в IEF хранятся секретные ключи для криптографии. IEF недоступны для внешнего приложения.
working elementary file (рабочий элементарный файл) / WEF
данные в нём может читать и писать внешнее приложение через считыватель.
short EF identifier (короткий идентификатор EF) / SFI
Специальный тип идентификаторов, используется только для элементарных файлов и состоит максимум из пяти битов (тридцать возможных значений от 00001 до 11110, значение 00000 ссылается на текущий выбранный EF, значение 11111 не используется). Этот идентификатор позволяет читать данные из файла без его предварительного выбора (то есть без вызова инструкции SELECT, об этом ниже).

Мы дальше будем работать только с WEF.

dedicated file (назначенный файл) / DF

Это нечто вроде контейнера, DF может содержать либо элементарные файлы (EF), либо другие назначенные файлы (DF). Неформально принятно DF называть каталогами (directories). Также в DF могут храниться данные. Все непосредственно вложенные EF и DF для данного DF должны иметь разные идентификаторы.

master file (главный файл) / MF
Это DF, выделенный в ISO/IEC 7816-4 как «главный» файл в иерархии. MF может отсутствовать, но если он есть, то его идентификатор — 3F 00.
DF name (имя DF)
DF можно назначить имя — байтовый массив размером до 16 байтов. В имени можно использовать любые байты, а не только ASCII-буквы. Имя должно быть уникально в пределах всей карты.
file identifier (идентификатор файла) / FID
Байтовый массив из двух элементов. Идентификаторы файлов, у которых общий родитель, должны быть разными. Идентификатор файла и его родителя должны быть разными. Глобально уникальными в пределах всей карты идентификаторы быть не обязаны.

ISO/IEC 7816 File System

Такую структуру MF, DF и EF часто неформально называют ISO file system (файловая система ISO). Русскоязычные названия типа «назначенный файл» являются официальными и взяты из стандарта ГОСТ Р ИЗО:МЭК 7816-4.

Описанная структура/иерархия объектов не является обязательной и в стандарте ISO/IEC 7816-4 приводится лишь как пример. Как следствие, на карте может находиться полноценная иерархия файлов с MF во главе и одновременно присутствовать DF или EF, не находящиеся в ней. Часто MF вообще отсутствует и клиентское ПО должно заранее знать идентификаторы или имена DF, чтобы обращаться к ним напрямую.

❈ ❈ ❈

application dedicated file / ADF
Специальный тип DF, в дереве под которым содержатся все EF и DF определённого карточного приложения. ADF может находится вне иерархии MF.
application identifier (идентификатор приложения) / AID
Идентификатор приложения — это имя ADF (то есть байтовая строка размером от 5 до 16 байтов).

AID имеет определённую структуру, первые пять байтов образуют RID — Registered Application Provider Identifier, а оставшиеся байты — PIX — Proprietary Application Identifier Extension. Application Provider — это обычно какая-нибудь фирма, корпорация или другая подобная структура. Application Provider может получить уникальный RID после обращения в уполномоченную организацию, порядок этих процедур описан в ISO/IEC 7816-5. По сути RID является идентификатором организации, которая может создавать на его базе свои AID, дописывая в конец произвольные байты (PIX). Подробнее о структуре AID/RID/PIX написано в разделе 8.2.1.2 Application identifier стандарта ISO/IEC 7816-4.

Пример: приложение для работы с Visa на банковской карте имеет AID=A0 00 00 00 03 20 10, при этом RID=A0 00 00 00 03 (RID организации VISA), а PIX=20 10. Другой пример, AID=A0 00 00 06 58 10 10, RID=A0 00 00 06 58 — это идентификатор платёжной системы МИР, PIX=10 10. Большой список RID можно найти здесь: https://www.eftlab.com.au/knowledge-base/complete-list-of-registered-application-provider-identifiers-rid.

Важно понимать, что собственно карточное приложение скрыто от внешнего клиента и доступно только через APDU, часть из которых является межотраслевыми (то есть определены в ISO/IEC 7816), а часть — проприетарными.

Обычно у конечного пользователя карты нет возможности создавать или удалять файлы. Да и в целом не стоит их воспринимать как статичные байтовые объекты, это всего лишь интерфейс взаимодействия с приложением смарт-карты.

❈ ❈ ❈

Работа с файлами (EF/DF) происходит через изменение текущего состояния карты, эта операция называется SELECT (выбор, выбрать файл). Инструкция SELECT описана в разделе 11.2.2 SELECT command ISO/IEC 7816-4, а процедура и логика процесса — в разделе 7.3 Structure selection.

SELECT «привязывает» файл к указанному в байте CLA каналу, обычно это нулевой, но есть карты, поддерживающие несколько каналов. Номер канала задаётся битами b1 и b2 в CLA. Однако это уже продвинутая тема и в реальности эта информация вам не пригодится, так как все используют канал по умолчанию.

Байт INSA4. Семантика параметров P1 и P2 детально описана в стандарте. После выбора можно отправлять APDU-команды для работы с этим файлом.

Для выбора можно использовать FID, имя DF, идентификатор EF/DF относительно текущего выбранного; можно выбирать «предка» текущего выбранного файла — это всё указывается битовыми масками в аргументе P1. Также можно выбирать файлы по пути из идентификаторов. Доступные способы выбора определяется производителем карты или карточного приложения, как правило, не все из них доступны.

Команда SELECT обычно возвращает байтовый массив, его структура задаётся в P2, всего возможны четыре вида ответов: пустой, File Control Information (FCI), File Control Parameters (FCP), File Management Data (FMD). Структура FCI, FCP и FCD описывается в разделе 7.4 File and data control information стандарта ISO/IEC 7816-4. Подробно об этих объектах я расскажу в последующих разделах и примерах кода.

Однако SELECT может возвращать данные и в другом формате, отличающемся от описанного в ISO/IEC 7816-4.

❈ ❈ ❈

После выбора EF (про DF чуть позднее) с ним можно работать (если карта позволяет это делать): читать, писать, удалять. Данные в файле могут быть в одной из трёх форм (русскоязычная терминология даётся по официальному переводу ГОСТ Р ИСО:МЭК 7816-4):

  • data units (единицы данных), раздел 11.3.1 ISO/IEC 7816-4
  • records (записи), раздел 11.4.1 ISO/IEC 7816-4
  • data objects (информационные объекты), раздел 11.5 ISO/IEC 7816-4

Соответствующие структуры EF:

  • Transparent structure (прозрачная структура, иногда называется бинарной или аморфной): никакой структуры нет, только единицы данных — блоки байтов. Размер блока является свойством конкретного EF. Для работы с такими файлами используются команды READ BINARY, WRITE BINARY и UPDATE BINARY.
  • Record structure (структура записи): данные представлены в виде отдельно идентифицируемых записей, они могут быть как фиксированной, так и переменной длины. Кроме того, записи могут быть организованы в циклическую структуру, где новые записи автоматические затирают самые старые, когда заканчивается выделенное место. Для работы используются команды READ RECORD (S), WRITE RECORD, UPDATE RECORD, APPEND RECORD, SEARCH RECORD, ERASE RECORD (S).
  • TLV structure (структура TLV): данные представлены в виде информационных объектов, TLV расшифровывается как Tag-length-value, подробнее о таких данных я расскажу в следующем разделе. Для работы используются команды PUT DATA и GET DATA.

❈ ❈ ❈

Внутри DF данные могут присутствовать только в форме информационных объектов (data objects), а внешнее приложение достаёт конкретные объекты по известным ему тегам.

Теория: кодирование информационных объектов

Информационный объект — это структурированные данные, закодированные в массив байтов. Стандарт ISO/IEC 7816-4 определяет два главных вида кодирования: SIMPLE-TLV и BER-TLV. Со словом TLV мы уже встречались в примере с NDEF, это расшифровывается как Tag-Length Value (Тег-Длина-Значение) и позволяет довольно просто кодировать структуры (где ключом выступает тег) в плоские байтовые массивы.

SIMPLE-TLV

Рассмотрим сначала более простой метод SIMPLE-TLV, закодированные в нём данные состоят из двух или трёх полей.

Сначала идёт поле с тегом длиной в один байт, в нём хранится значение от 0x01 до 0xFE (0x00 и 0xFF не допускаются), которое является номером тега (tag number).

Затем идёт один или три байта, задающих длину блока данных. Если первый байт из них имеет значение от 0x00 до 0xFE включительно, то это и есть длина блока данных. Если первый байт имеет значение 0xFF, то длину определяют следующие два байта, задающие число в формате big endian от 0 до 65535.

И собственно поле с байтовыми данными указанной длины. Вот примеры закодированных таким способом данных (лишние пробелы я расставил для читабельности):

  • 84 07 A0 00 00 02 77 10 10 — номер тега: 0x84, длина: 7 байтов, данные: A0 00 00 02 77 10 10
  • 21 00 — есть только тег с номером 0x21, длина блока данных нулевая

Этот тип кодирования не определяет структуру блока с данными, для него это просто набор байтов неизвестной структуры. Другими словами, парсер не сможет автоматически понять, что данные внутри значения тоже как-то закодированы.

BER-TLV

Формат BER-TLV определён в стандарте ASN.1 (ISO/IEC 8825-1) и в ISO/IEC 7816 используется его подмножество. BER расшифровывается как Basic Encoding Rules и работает по такому же принципу, как SIMPLE-TLV, только позволяет использовать более длинные теги и блоки данных. То есть информационный объект состоит из байтов тега, байтов с длиной данных и собственно байтов с данными.

ASN.1 (Abstract Syntax Notation One) — это язык описания структурированных данных, которые потом по этому описанию можно сериализовать в байтовый массив и десериализовать обратно. ASN.1 широко используется в сетевом программировании и криптографии. BER-TLV является одним из способов кодирования описанных языком ASN.1 структур в байтовый массив.

Далее я опишу метод BER-TLV в том виде, в каком он используется в ISO/IEC 7816.

❈ ❈ ❈

Закодированные данные состоят из трёх байтовых блоков: Tag, Length и Value, которые идут подряд друг за другом.

Размер блока с тегом может быть 1, 2 или 3 байта. В SIMPLE-TLV это был только один байт и он же являлся идентификатором (номером) типа данных. Однако в BER-TLV байты тега имеют определённую битовую структуру, чтобы адекватно передать описания из ASN.1.

Два старших бита b8 и b7 первого байта определяют класс тега:

  • 0 0 обозначает универсальный класс, он определён в спецификации ASN.1, в универсальный класс входят, например, целые значения, строки и так далее
  • 0 1 обозначает класс приложения, эти теги определены в стандарте ISO/IEC 7816
  • 1 0 обозначает контекстно-зависимый класс, эти теги определены в стандарте в ISO/IEC 7816
  • 1 1 обозначает приватный класс, теги такого класса в ISO/IEC 7816 отсутствуют

Понятие класса тега идёт глубоко из спецификации ASN.1.

Тег универсального класса означает, что поле с данными обязательно должно соответствовать указанному универсальному типу (то есть номеру тега) из спецификации ASN.1.

А вот все остальные классы могут быть произвольными и на их использование стандарт не накладывает никаких ограничений. Однако приняты следующие соглашения насчёт типов данных для тегов разных классов.

Тег класса приложения (application class) определяет тип данных, специфичный для данной предметной области, он уникален в её пределах, задаёт данные определённой природы и не используется ни для чего другого. В нашем случае теги класса приложения задаются в ISO/IEC 7816 и они должны в таком же виде использоваться другими стандартами.

Теги приватного класса (private class) обычно не используется вообще.

Тег контекстно-зависимого класса (context-specific) определяет тип данных, семантика которого определяется контекстом, этот класс используется чаще всего.

Подробно о классах написано в главе Chapter 4 Tagging книги ASN.1 Complete (стр. 172).

Бит b6 первого байта определяет структуру блока данных:

  • 1 означает, что блок данных не просто байтовый массив, но закодирован через BER-TLV (составные данные, constructed);
  • 0 означает, что блок данных — это просто массив байтов неизвестной структуры, примитивное кодирование, примитивные данные (primitive).

Биты первого байта с b5 по b1 используются так:

  • Если среди битов с b5 по b1 есть хотя бы один 0, то полученное число является номером тега и тег на этом заканчивается, в этом случае номер тега лежит в диапазоне от 00000 до 11110 (в десятичной системе от 0 до 30, в hex от 0x00 до 0x1E).
  • 1 1 1 1 1 означает, что тег продолжается в следующем или нескольких следующих байтах и в подсчётах номера тега не участвует. В каждом последующем байте значение бита b8=1 означает, что этот байт входит в тег. Значение b8=0 означает, что этот байт последний в теге. Биты с b7 по b1 конкатенируются по всем участвующим байтам и формируют номер тега.

ISO/IEC 7816 допускает максимум три байта для тега, бо́льшее количество оставлено на будущее. Тремя байтами можно закодировать номера тегов с 0 до 16383 (hex 00 — 3F FF).

Это очень важный момент: в отличие от SIMPLE-TLV, где в байтах тега записан только номер тега, в BER-TLV в этом поле записаны тип кодирования данных, номер тега и класс тега; эта тройка определяет тип данных. Но для практического использования это всё знать не нужно, достаточно только корректно разобрать бинарные данные, выделить тег и дальше смотреть в стандартах, какие данные должны лежать в блоке данных этого тега.

Вот два примера: теги 0x6F и 0x9F38.

BER-TLV tags example

❈ ❈ ❈

Дальше идут байты, определяющие длину блока данных.

Если бит b8 первого байта равен 0, то последующие биты задают длину блока данных. В этом случае можно закодировать число от 0 до 127.

Если бит b8 первого байта равен 1, то последующие биты задают количество следующих байтов, определяющих длину блока данных.

В ISO/IEC 7816-4 максимально можно использовать пять байтов для кодирования длины, включая первый байт.

❈ ❈ ❈

И собственно данные. Если в теге указано, что данные примитивные (primitive), то этот массив байтов передаётся как есть и должен каким-то образом интерпретироваться программой.

Если же данные составные (constructed), то в этом блоке идут подряд несколько закодированных в BER-TLV объектов (я их дальше буду называть полями, это неформальное и нестандартное обозначение, просто так удобно). Можно такой блок воспринимать как массив вложенных BER-TLV объектов, дерево. Однако имейте в виду, что в блоке может быть несколько объектов с одинаковыми тегами, то есть нельзя воспринимать их как структуру с полями-тегами, это именно упорядоченный список.

❈ ❈ ❈

Множество информационных объектов BER-TLV, образующих цельную смысловую единицу, называют шаблоном (template). ISO/IEC 7816 определяет несколько межотраслевых шаблонов, а другие спецификации дополнительно определяют свои. В описании операции может быть указано, что она возвращает, скажем, Шаблон FCI; это значит, что где-то рядом должно быть описание этого шаблона, из каких BER-TLV объектов он состоит и как их значения интерпретировать.

Для каждого из шаблонов зарезервирован собственный тег класса приложения. Если вы встречаете BER-TLV объект с таким тегом, то можете с уверенностью предполагать, что структура объекта соответствует описанию подходящего шаблона.

❈ ❈ ❈

Вот несколько примеров кодирования данных через BER-TLV.

50 07 49 4E 54 45 52 41 43
  Tag: 50 (01010000, application class, tag number: 0x10, primitive data encoding)
  Length: 07
  Data: 49 4E 54 45 52 41 43 (ASCII value: "INTERAC")


BF 3D 05 86 02 01 00
  Tag: BF3D (10111111 00111101, context-specific, tag number: 0x3D, BER-TLV data)
  Length: 04
  Data: 86 02 01 00
        Tag: 86 (100001100, context-specific, tag number 0x6, primitive data encoding)
        Length: 02
        Data: 01 00

❈ ❈ ❈

Закодированные в BER-TLV байты можно полностью распарсить в рекурсивную структуру ключ-значение, где в качестве ключа выступает бинарное значение тега, а в качестве значения либо массив байтов, если тег определяет primitive data, либо список таких же структур, если тег определяет constructed data.

Подробно BER-TLV описан в разделе 6.3 BER-TLV data objects стандарта ISO/IEC 7816-4, а также в разделе 8.1.2 стандарта ASN.1 ISO/IEC 8825-1 (он же X.680), скачать актуальную на текущий момент версию можно с официального сайта: https://www.itu.int/ITU-T/recommendations/rec.aspx?rec=14468.

example-12: Чтение данных ISO/IEC 7816-4 на примере банковской карты

Исходный код программы: example-12/emv-card-read

Для этого примера можно использовать оба типа считывателей: для бесконтактных и контактных чиповых карт. Также понадобится любая банковская карта с чипом.

Цели этого примера:

  • показать, как кодируются и декодируются данные в BER-TLV;
  • продемонстрировать, как выглядит работа с файлами на карте по стандарту ISO/IEC 7816-4;
  • показать, как работать со стандартом EMV.

Сразу замечание. Если попытаетесь выполнить инструкции из этого примера на карте памяти (например, SLE4442 или Mifare Classic), то получите ошибку на уровне библиотеки, так как карта таких команд просто не понимает.

Обработка результата выполнения команды в протоколе T=0

Выше в разделе Протоколы обмена данными я упоминал, что бывают два протокола для передачи данных: T=0 и T=1. При работе с контактными картами часто поддерживается только T=0 и при его использовании есть особенность передачи данных ответа: они могут возвращаться не сразу после команды, а в результате последовательных вызовов отдельной команды GET RESPONSE.

Для обработки такой ситуации я сделал отдельную функцию transmit_wrapper, которая собирает результат за несколько запросов и возвращает его так, как будто он был возвращён весь сразу.

def transmit_wrapper(connection, apdu):
    response, sw1, sw2 = connection.transmit(apdu)
    if sw1 == 0x61:
        response_data = []
        ne = sw2
        while True:
            gr_apdu = '00 C0 00 00 {:02X}'.format(ne)
            response, sw1, sw2 = connection.transmit(toBytes(gr_apdu))
            if (sw1,sw2) == (0x90,0x00):
                response_data.extend(response)
                break
            elif sw1 == 0x61:
                response_data.extend(response)
                ne = sw2
                continue
            else:
                # error, pass sw1, sw2 back to caller
                response_data = []
                break

        return response_data, 0x90, 0x00
    else:
        return response, sw1, sw2

Значение 61 XX в статусном слове означает, что есть ещё данные, которые команда может вернуть, в этом случае в XX кодируется то количество, которое доступно. Для получения данных мы вызываем команду GET RESPONSE с кодом C0 и в ней указываем в поле Le значение XX. Эта команда также может вернуть статусное слово 61 XX, так что мы вызываем GET RESPONSE до тех пор пока не получим все данные (о чём будет сигнализировать статусное слово 90 00).

Если вас интересует, как именно организована передача данных в протоколе T=0, рекомендую прочитать эту статью на хабре.

Общие принципы работы с банковской картой

Банковские карты подчиняются стандарту EMV, его полная спецификация доступна по адресу https://www.emvco.com/emv-technologies/contact/. Все документы разбиты на несколько книг (books), освещающих разные аспекты стандарта.

Процесс работы банковского-терминала (IFD, это может быть банкомат или POS-терминал в магазине) с банковской картой (ICC) состоит из нескольких этапов:

  1. ICC вставляется в IFD, IFD распознаёт подключение и активирует электрические контакты.
  2. Происходит операция RESET для ICC, IFD получает из карты параметры карты и устанавливает соединение.
  3. Выполняется транзакция (или несколько транзакций).
  4. Карта отключается от контактов и вынимается из считывателя терминала.

Так как мы работает внутри инфраструктуры PC/SC, то пункты 1 и 2 происходит внутри считывателя без нашего участия, примерно так же с пунктом 4. Остаётся пункт 3.

Пункт 3 разбивается на такие этапы:

  1. Выбор приложения (application) — описывается в EMV_v4.3, книга 1, раздел 12 Application Selection.
  2. Выполнение команд транзакций — описывается в EMV_v4.3, книга 3, раздел 10 Functions Used in Transaction Processing.

Банковская карта: выбор приложения

Файлы каждой платёжной системы (например, Visa или Mastercard) должны располагаться внутри собственного DF, называемого Application Definition File (ADF). У этих ADF должны быть фиксированные имена (ADF Name), которые централизованно назначаются специальной организацией. Выше я уже писал, что такие имена называются Application Identifier (AID) и они предопределены для Visa, Mastercard или другой системы. Как правило на карте есть только один ADF для одной платёжной системы.

IFD должен найти приложение платёжной системы на карте и выбрать его для дальнейшей работы. Сделать это можно двумя способами.

Во-первых, на карте может присутствовать специальный DF с именем 1PAY.SYS.DDF01, по EMV он называется PSE (Payment System Environment), в этом «каталоге» хранится список идентификаторов записанных на карту ADF платёжных систем. Наличие PSE не гарантируется, кроме того, он может являться MF, но не обязан.

Во-вторых, если программа не находит PSE, она пытается перебрать все знакомые ей AID платёжных систем. Эта процедура использования PSE описана в разделе 12.3.2 Using the PSE книги 1 EMV_v4.3.

Для бесконтактных карт используется DF со специальным именем 2PAY.SYS.DDF01, он должен присутствовать всегда.

Поиск ADF через PSE

C-APDU для выбора DF с именем 1PAY.SYS.DDF01 выглядит так (команда SELECT, ISO/IEC 7816-4, раздел 11.2.2 SELECT command):

CLA  INS  P1 P2   Lc  Data
00   A4   04 00   0E  31 50 41 59 2E 53 59 53 2E 44 44 46 30 31

Здесь P1=04 означает, что в поле DATA передаём имя DF (DF name), а для приложения имя ADF является его идентификатором. Имя передаём как байтовую строку, закодированную в ASCII.

P2=00 означает (см. Таблицу 63 в ISO/IEC 7816-4), что мы хотим получить ответ в виде шаблона FCI (FCI template) для первого или единственного результата.

Ранее для нас любой результат в статусном слове (SW) помимо 0x9000 рассматривался как ошибка, но в этот раз мы должны также обработать статусное слово 6A 82, что в контексте EMV означает Файл PSE не найден. В этом случае мы сначала попробуем найти PSE через имя 2PAY.SYS.DDF01 (для бесконтактных карт), а если это не удалось, то будем нужное приложение искать перебором заранее известных идентификатов. В случае остальных ошибок предполагаем, что мы такую карту не поддерживаем.

# Select PSE first
#       CLA INS P1  P2  Lc  DATA
apdu = '00  A4  04  00  0E  31 50 41 59 2E 53 59 53 2E 44 44 46 30 31'
response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
if (sw1,sw2) == (0x90,0x00):
    # take AID from FCI
    pse_data = response
    aid = get_AID_from_PSEFCI(cardservice.connection, pse_data)
    if aid is None:
        aid = guess_AID(cardservice.connection)
elif (sw1,sw2) == (0x6A,0x82):
    # try with PPSE "2PAY.SYS.DDF01" for contactless cards
    apdu = '00  A4  04  00  0E  32 50 41 59 2E 53 59 53 2E 44 44 46 30 31'
    response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
    if (sw1,sw2) == (0x90,0x00):
        # take AID from FCI
        pse_data = response
        aid = get_AID_from_PSEFCI(cardservice.connection, pse_data)
        if aid is None:
            aid = guess_AID(cardservice.connection)
    elif (sw1,sw2) == (0x6A,0x82):
        # guess AID
        print('guessed')
        aid = guess_AID(cardservice.connection)
else:
    print('Failed to read card.')
    print('{:02X} {:02X}'.format(sw1,sw2))
    return 1

Для получения AID мы используем две функции: get_AID_from_PSEFCI для выделения AID из структуры PSE и guess_AID для угадывания (точнее, перебора известных) AID.

Разбор FCI template (get_AID_from_PSEFCI)

Результатом выполнения инструкции SELECT являются данные, соответствующие FCI template, этот шаблон описан в разделе 7.4.1 File control information retrieval стандарта ISO/IEC 7816-4. В моём случае считыватель вернул вот такой массив (я его разбил для удобства на группы по 16 байтов):

6F 28 84 0E 31 50 41 59 2E 53 59 53 2E 44 44 46
30 31 A5 16 88 01 01 5F 2D 08 72 75 65 6E 66 72 
64 65 BF 0C 05 9F 4D 02 0B 0A

Разберём его один раз вручную, чтобы показать, как это делается.

Сразу отмечу, что инструкция SELECT возвращает BER-TLV объект, а так как мы запросили Шаблон FCI, то у этого объекта ожидается соответствующий тег — 6F. Стандарт определяет Шаблон FCI (FCI template) как совокупность контрольных параметров файла и контрольной информации файла.

Если посмотрим на двоичное представление 6F = 0110 1111, то увидим следующее (см. раздел выше с описанием BER-TLV):

  • биты b8=0 и b7=1 означают класс тега, в нашем случае это класс приложения;
  • бит b6=1 означает, что данные закодированы в BER-TLV;
  • биты b5..b1=01111 кодируют номер тега, в нашем случае это 0xF, то есть 15 в десятичном виде, а блок с тегом этим заканчивается, так как среди битов b5..b6 есть как минимум один ноль.

Следующим блоком идёт длина блока данных, 0x28 = 0010 1000, бит b8=0 означает, что в остальных битах закодирована длина блока данных и блок с длиной на этом заканчивается. То есть длина блока данных — сорок байтов (0x28 = 40).

Смотрим следующий байт (первый байт блока с ответом) — 0x84 = 1000 0100, это тег, контекстно-зависимый класс, данные не закодированы, номер тега — 4. И следующий байт — длина — 0x0E, то есть 14 байтов — 31 50 41 59 2E 53 59 53 2E 44 44 46 30 31.

Но данные на этом не закончились и мы должны продолжить разбор дальше. Новый блок начинается байтом 0xA5 = 1010 0101, это тег, контекстно-зависимый класс, данные закодированы в BER-TLV, номер тега 5. Следующим блоком идёт длина данных — 0x16, это 22 байта, как раз до конца всех данных.

В ISO/IEC 7816-1 не определён общий способ, которым DF ссылается на «принадлежащие» ему файлы и этот процесс отдаётся на усмотрение сторонним спецификациям. Обычно DF ссылается на EF, в котором перечисляются другие EF или DF, такой файл принято обозначать DIR. В случае EMV файлы приложения задаются через объект с проприетарным тегом A5.

Для целей данного примера я написал свой достаточно простой разборщик BER-TLV (см. bertlv.py). В этом модуле реализованы:

  • класс Tlv, который инкапсулирует один TLV-объект, в поле tag хранится собственно тег в виде целого числа (байты тега представляют собой его big-endian форму), а в поле value — байтовый массив, если TLV-объект примитивный (primitive), и список вложенных объектов класса Tlv, если TLV-объект составной (constructed). Вложенные TLV-объекты я также буду называть полями или частями;
  • функция parse_bytes, которая принимает на вход байтовый список и возвращает список закодированных в нём TLV-объектов;
  • функция find_tag, которая принимает на вход тег в виде целого числа и список объектов класса Tlv, а возвращает первый найденный объект с указанным тегом, или None, если ничего не найдено.

❈ ❈ ❈

Начнём с функции get_AID_from_PSEFCI, целиком её код в тексте программы example-12/emv-card-read, а здесь только важные фрагменты с комментариями.

На входе байтовый массив с FCI, его структура описана в ISO/IEC 7816-4, раздел 7.4.1 File control information retrieval. Это BER-TLV объект с тегом 6F, он содержит другие объекты с разными тегами, но нужная нам информация хранится в блоке с тегом A5, это специальный тег для проприетарных данных, структура которого определяется каким-то другим стандартом (не ISO/IEC 7816). В нашем случае это стандарт EMV_v4.3, книга 1, раздел 11.3.4 Data Field Returned in the Response Message. Вот как мы запрашиваем вложенный объект с тегом A5:

parts = bertlv.parse_bytes(data)
t = bertlv.find_tag(0x6F, parts)
if t is None:
    # expecting FCI template
    return None
# pi means "proprietary information"
piTlv = bertlv.find_tag(0xA5, t.value)
if piTlv is None:
    # Cannot find EMV block in PSE FCI
    return None

В объекте piTlv из всех возможных полей нас интересует поле с тегом 88, это контекстно-зависимый класс, взятый из ISO/IEC 7816-4. Тег помечает Short EF identifier (SFI) и данный объект содержит короткий идентификатор EF, это число в диапазоне от 1 до 30 включительно, идентифицирующее какой-то конкретный файл в границах карты. По SFI с этим файлом можно выполнять отдельные операции без необходимости предварительного выбора файла через инструкцию SELECT. В нашей ситуации в этом файле содержится SFI Payment System Directory (PSD), то есть каталог всех платёжных систем на карте.

Вот как мы читаем значение SFI:

sfiTlv = bertlv.find_tag(0x88, piTlv.value)
if sfiTlv is None:
    # Cannot find SFI block in PSE FCI
    return None
defSfiData = sfiTlv.value
sfi = defSfiData[0]

Теперь мы можем прочитать PSD. Это линейный файл с записями (linear record structure, ISO/IEC 7816-4, раздел 7.3.4 Data referencing methods in elementary files). Мы должны прочитать все записи из файла, это делается инструкцией READ RECORD из ISO/IEC 7816-4, раздел 11.4.3 READ RECORD (S) command. Базовый APDU этой команды в коде задаётся так:

p2 = (sfi << 3) | 4
#       CLA INS P1  P2      Le
apdu = '00  B2  00  {:02X}  00'.format(p2)
apduTpl = toBytes(apdu)

Мы должны в аргументе P2 передать следующие данные в соответствующих битовых фрагментах (детали описаны в ISO/IEC 7816-4, таблицы 71 и 72) следующее:

  • файл по его SFI, а не текущий выбранный файл (записать SFI в старшие пять битов P2);
  • прочитать запись по номеру из P1 (записать биты 1 0 0 в младшие три бита P2).

Это делается в коде p2 = (sfi << 3) | 4.

Теперь будем последовательно выполнять эту инструкцию с увеличивающимся номером записи, начиная с 1. Здесь есть нюанс: в APDU мы указали байт Le=0, что означает вернуть все данные сразу, однако в некоторых картах команда может вернуть ошибку 6CXX в статусном слове, означающую, что нужно повторить запрос, только вместо Le=0 указать точную длину Le=XX, здесь XX — это второй байт из статусного слова (то есть SW2).

Структура каждой записи определена в EMV_v4.3, книга 1, раздел 12.2.3 Coding of a Payment System Directory: объект с тегом 70, внутри несколько объектов с тегом 61, внутри которого в поле с тегом 4F лежит имя приложения платёжной системы (Application Definition File name, ADF name), оно нам и нужно.

foundAIDs = []
recordNumber = 1
expectedLength = 0
while True:
    apdu = apduTpl
    apdu[2] = recordNumber  # P1
    apdu[4] = expectedLength  # Le
    response, sw1, sw2 = transmit_wrapper(connection, apdu)
    if sw1 == 0x6C:
        expectedLength = sw2
        continue
    if (sw1,sw2) != (0x90,0x00):
        break
    if len(response) > 0:
        parts = bertlv.parse_bytes(response)
        psd = bertlv.find_tag(0x70, parts)
        # psd must have tag 0x70
        # see EMV_v4.3 book 1, section "12.2.3 Coding of a Payment System Directory"
        if psd is None:
            return None
        for t in psd.value:
            if t.tag == 0x61:
                aidTlv = bertlv.find_tag(0x4F, t.value)
                if aidTlv is not None:
                    foundAIDs.append(aidTlv.value)
    recordNumber += 1
    expectedLength = 0

Вот в этом фрагменте мы снова перескакиваем на начало цикла, если в SW1 находится 0x6C, но сначала указываем полученное значение expectedLength из SW2, а в конце цикла не забываем опять обнулить expectedLength:

    if sw1 == 0x6C:
        expectedLength = sw2
        continue

Собранные идентификаторы AID сохраняем в список foundAIDs, в конце просто берём из него первый элемент и возвращаем, а если там пусто, то возвращаем None.

Описанный здесь подход разбора шаблона категорически нельзя использовать в реальном промышленном коде. Суть BER-TLV в том, чтобы разбирать закодированные в нём данные автоматически в соответствии с формальным описанием на языке ASN.1. А ручной разбор с кучей проверок и «спуском» вниз по иерархии с корневого BER-TLV объекта является излишне многословным и потенциально может привести к серьёзным ошибкам. Поэтому воспринимайте этот код только как обучающий.

В идеале у вас должны быть ASN.1-спецификации всех шаблонов и код, который на входе получает байтовый массив и спецификацию шаблона, а на выходе либо возвращает разобранный объект с полностью разобранными данными, либо выбрасывает исключение с описанием ошибки.

Поиск ADF без PSE

Теперь рассмотрим ситуацию, когда на карте нет PSE (Payment System Environment). В этом случае EMV_v4.3 предписывает (книга 1, раздел 12.3.3 Using a List of AIDs) перебор DF по известным идентификаторам платёжных систем.

Код этой функции тривиальный: просто проходим по списку предопределённых AID и пытаемся выбрать DF с таким именем.

def guess_AID(connection):
    print('guess')
    candidateAIDs = [
        'A0 00 00 00 03 20 10',  # Visa Electron
        'A0 00 00 00 03 10 10',  # Visa Classic
        'A0 00 00 00 04 10 10',  # Mastercard
        'A0 00 00 06 58 10 10',  # MIR Credit
        'A0 00 00 06 58 20 10'   # MIR Debit
    ]
    foundAID = None
    #          CLA INS P1  P2  Le
    apduTpl = '00  A4  04  00  07'
    for aid in candidateAIDs:
        apdu = apduTpl + aid
        response, sw1, sw2 = transmit_wrapper(connection, toBytes(apdu))
        if (sw1,sw2) == (0x90,0x00):
            foundAID = toBytes(aid)
            break

    return foundAID

Выбор ADF и разбор его FCI

Теперь, когда AID платёжного приложения известен, можно начинать с ним работу. Так как мы пишем не настоящий банковский терминал, ограничимся только вытаскиванием свободно лежащих данных.

Выбор DF приложения делается так же, как и выбор DF PSE — через команду SELECT. Продолжим с полученным на прошлом этапе AID в переменной aid:

apdu = '00  A4  04  00  07  ' + toHexString(aid)
response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('No EMV app found.')
    return 1

parts = bertlv.parse_bytes(response)
fciTlv = bertlv.find_tag(0x6F, parts)

Результатом этих команд является FCI Template, для моей карты этот набор байтов выглядит так:

6F 2C 84 07 A0 00 00 00 03 10 10 A5 21 50 04 56
49 53 41 5F 2D 04 72 75 65 6E 87 01 01 9F 11 01
01 9F 12 0A 56 69 73 61 20 44 65 62 69 74

После декодирования получаем следующую TLV структуру:

6F  8407A0000000031010A5215004564953415F2D047275656E8701019F1101019F120A56697361204465626974
 +- 84  A0000000031010  // Dedicated File (DF) Name
 +- A5  5004564953415F2D047275656E8701019F1101019F120A56697361204465626974 // FCI Proprietary Template
     +- 50    56495341  // Application Label, "VISA"
     +- 5F2D  7275656E  // Language Preference, "ruen"
     +- 87    01  // Application Priority Indicator
     +- 9F11  01  // Issuer Code Table Index
     +- 9F12  56697361204465626974 // Application Preferred Name, "Visa Debit"

Полностью этот шаблон описан в стандарте EMV_v4.3, книга 1, раздел 11.3.4 Data Field Returned in the Response Message, таблица Table 45: SELECT Response Message Data Field (FCI) of an ADF.

В элементе с тегом A5 находится информация, специфичная для EMV. Вот элементы из нашего примера ((M) означает mandatory, обязательное поле; (O) — optional, опциональное):

  • тег 0x50Application name (M) — здесь хранится название платёжной системы, в данном случае VISA;
  • тег 0x5F2DLanguage Preference (O) — здесь хранится предпочитаемый язык в виде максимум четырёх двухбуквенных кодов из ISO 639, в данном случае 72 75 65 6E это строка "ruen";
  • тег 0x87Application Priority Indicator (O) — индикатор приоритета приложения относительно других приложений на этой же карте, описан в таблице там же Table 48: Format of Application Priority Indicator;
  • тег 0x9F11Issuer Code Table Index (O) — кодовая таблица символов для отображения Application Preferred Name (согласно ISO/IEC 8859), в нашем случае значение 0x01 означает ISO/IEC 8859-1, то есть кодировку latin-1
  • тег 0x9F12Application Preferred Name (O), здесь хранится название приложения, которое может использовать терминал для отображения в дополнение к Application name, у нас тут строка "Visa Debit"

Дополнительно могут содержаться другие поля, например:

  • тег 0xBF0CFCI Issuer Discretionary Data (O) — в это поле включается вся остальная информация от производителя карты, банка, производителя приложения, карточной операционной системы и так далее, допустимые теги и их семантика определены в EMV_v4.3, книга 3, раздел Annex B Rules for BER-TLV Data Objects.
  • тег 0x9F38PDOL (O) — Processing Options Data Object List, содержит специальные данные, необходимые для начала финансовой транзакции. Это поле очень важное, в нём приложение указывает, какие дополнительные данные ему нужны для инициализации финансовой транзакции, я ниже покажу, как это значение использовать.

Другие примеры FCI Template:

6F 48 84 07 A0 00 00 00 04 10 10 A5 3D 50 0A 4D
61 73 74 65 72 43 61 72 64 87 01 01 9F 11 01 01
9F 12 0A 4D 61 73 74 65 72 43 61 72 64 5F 2D 08
72 75 65 6E 66 72 64 65 BF 0C 0F 9F 4D 02 0B 0A
9F 6E 07 06 43 00 00 30 30 00
6F  8407A0000.......000
 +- 84  A0000000041010  // Mastercard
 +- A5  500A4D6173746572436172648701019F1101019........0B0A9F6E0706430000303000
     +- 50  4D617374657243617264  // "MasterCard"
     +- 87    01  // Application Priority Indicator
     +- 9F11  01  // Issuer Code Table Index
     +- 9F12  4D617374657243617264 // Application Preferred Name, "MasterCard"
     +- 5F2D  7275656E66726465  // Language Preference, "ruenfrde"
     +- BF0C  9F4D020B0A9F6E0706430000303000  // FCI Issuer Discretionary Data
         +-  9F4D  0B0A  // Log Entry
         +-  9F6E  06430000303000  // Third Party Data / Form Factor Indicator

И ещё:

6F 1F 84 07 A0 00 00 00 03 10 10 A5 14 50 0C 56
69 73 61 20 43 6C 61 73 73 69 63 9F 38 03 9F 1A
02
6F  8407A0000000031010A514500C5669736120436C61737369639F38039F1A02
 +- 84  A0000000031010  // Visa International - VISA Debit/Credit (Classic)
 +- A5  500C5669736120436C61737369639F38039F1A02
     +- 50  5669736120436C6173736963 // Application Label, "Visa Classic"
     +- 9F38  9F1A02  // Processing Options Data Object List (PDOL) 

И ещё пример с картой МИР:

6F 34 84 07 A0 00 00 06 58 10 10 A5 29 87 01 01
9F 38 0F 9F 7A 01 5F 2A 02 9F 02 06 9F 35 01 9F
40 05 5F 2D 04 72 75 65 6E BF 0C 05 9F 4D 02 18
0A 50 03 4D 49 52
6F  8407A0000006581010A5298701019F380F9F7A015F2A029F02069F35019F40055F2D047275656EBF0C059F4D02180A50034D4952
 +- 84  A0000006581010  // MIR Credit
 +- A5  8701019F380F9F7A015F2A029F02069F35019F40055F2D047275656EBF0C059F4D02180A50034D4952
     +- 87  01  Application Priority Indicator
     +- 9F38  9F7A015F2A029F02069F35019F4005  // Processing Options Data Object List (PDOL)
     +- 5F2D  7275656E  // Language Preference, "ruen"
     +- BF0C  9F4D02180A  // File Control Information (FCI) Issuer Discretionary Data
         +- 9F4D  180A  // Log Entry
     +- 50  4D4952   // Application Label, "MIR"

❈ ❈ ❈

Прочитаем и распарсим содержимое элемента с тегом 0xA5, здесь мы просто пройдём по всем вложенным объектам и распечатаем каждый, который знаем:

piTlv = bertlv.find_tag(0xA5, fciTlv.value)

# read all fields from FCI Proprietary Template
pdolData = None
for t in piTlv.value:
    if t.tag == 0x50:
        print('Application name: {}'.format(HexListToBinString(t.value)))
    elif t.tag == 0x87:
        print('Application Priority Indicator: priority={}, confirmation required={}'
            .format(t.value[0] & 0x0F, (t.value[0] >> 7 == 1)))
    elif t.tag == 0x9F38:
        print('PDOL is present')
        pdolData = t.value
    elif t.tag == 0x5F2D:
        print('Language preference: {}'.format(HexListToBinString(t.value)))
    elif t.tag == 0x9F11:
        print('Issuer Code Table Index: ISO 8859-{}'.format(t.value[0]))
    elif t.tag == 0x9F12:
        print('Application Preferred Name: {}'.format(HexListToBinString(t.value)))
    elif t.tag == 0xBF0C:
        print('FCI Issuer Discretionary Data is present')
    else:
        print('Unknown tag {:X}: '.format(t.tag, toHexString(t.value)))

Старт финансовой транзакции через GET PROCESSING OPTIONS

После выбора платёжного приложения начинается финансовая транзакция (Financial Transaction) — это отдельный набор команд и алгоритмов, описанный в книге 3 EMV_v4.3. Сам термин определяется следующим образом: Financial Transaction — The act between a cardholder and a merchant or acquirer that results in the exchange of goods or services against payment (Финансовая транзакция — действие между держателем карты и продавцом или эквайером, приводящее к обмену товаров или услуг за плату).

Первой командой в финансовой транзакции идёт GET PROCESSING OPTIONS, она описана в разделе 6.5.8 GET PROCESSING OPTIONS Command-Response APDUs книги 3 EMV_v4.3. C-APDU этой команды выглядит так:

CLA  INS  P1 P2   Lc    Data         Le
80   A8   00 00   [Lc]  XX XX .. XX  0

Сразу обратите внимание, что класс команды здесь проприетарный — 80. Аргументы P1 и P2 должны быть выставлены в 0. В поле Data передаётся специально сформированный байтовый массив DOL (Data Object List) в виде BER-TLV объекта с тегом 83.

Полностью схема построения этого массива описана в разделе 5.4 Rules for Using a Data Object List (DOL) книги 3 EMV_v4.3, а я расскажу коротко. Смысл DOL в том, чтобы передать в карту некоторые нужные ей параметры для старта финансовой транзакции. Имена (точнее, значения-теги) нужных параметров закодированы специальным образом в поле PDOL из FCI, полученного при выборе ADF.

Структура PDOL похожа на BER-TLV: тег, длина, но блока данных нет, вместо него сразу начинается следующий тег. Можно это представить так:

{T1} {L1}   {T2} {L2}   {T3} {L3}  ...   {Tn} {Ln}

Размер каждого {Ti} один или два байта (они формируются по таким же правилам, как теги в BER-TLV), размер {Li} — один байт. PDOL определяет, какую дополнительную информацию от нас ждёт карта, чтобы корректно выполнить команду. Например, тег 0x9F35 задаёт тип терминала (Terminal Type), а тег 0x9F1A — двухсимвольный код страны. Типы и значения конкретных параметров зависят от платёжной системы и какого-то их единого реестра не существует.

Чтобы стало понятнее, вот пример PDOL из карты МИР:

9F 7A 01 5F 2A 02 9F 02 06 9F 35 01 9F 40 05

Разобъём на компоненты:

{T1}   {L1} {T2}   {L2} {T3}   {L3} {T4}   {L4} {T5}   {L5}
{9F7A} {01} {5F2A} {02} {9F02} {06} {9F35} {01} {9F40} {05}

DOL формируется только из байтов данных (без тегов), идущих подряд в том же порядке, что элементы в PDOL. Перед данными указывается тег 83 и байт с общей длиной, равной {L1} + {L2} + ... + {Ln}. Например, для примера выше теги имеют следующие значения:

ТегДлинаОписание
0x9F7A 1 VLP Terminal Support Indicator, определяет, поддерживает ли терминал функциональность VLP (Visa Low-Value Payment), это возможность проведения очень быстрых транзакций на небольший суммы.
0x5F2A 2 Transaction Currency Code, задаёт код валюты транзакции по ISO 4217
0x9F02 6 Amount, Authorised (Numeric), задаёт авторизованный размер транзакции, кодируется по правилам, определённым в разделе 4.3 Книги 3 EMV_v4.3 (тип n)
0x9F35 1 Terminal Type, задаёт тип терминала, его возможности, кодируется в соответствии с приложением A1 книги 4 EMV_v4.3
0x9F40 5 Additional Terminal Capabilities, дополнительные возможности терминала, кодируется в соответствии с приложением A3 книги 4 EMV_v4.3

Программа-терминал по этому списку может сформировать и передать значение из 15 (1+2+6+1+5) байтов, указав в каждом из фрагментов нужные значения. Если программа не знает PDOL-тег или не хочет его заполнять, то может передать заполненный нулями фрагмент соответствующей длины. Мы именно так и поступим для простоты.

Если поля PDOL в FCI ADF не было, то передаём Lc=02 и 83 00, то есть C-APDU: 80 A8 00 00 02 83 00 00. Вы не можете передать 83 00, если PDOL был указан.

Подготовим DOL (создадим заполненный нулями массив нужной длины в переменной dolData):

dolData = [0x83, 0x00]
if pdolData is not None:
    # parse PDOL data
    lengthByte = False
    totalLength = 0
    for b in pdolData:
        if lengthByte:
            totalLength += b
            lengthByte = False
            continue
        if b & 0x1F != 0x1F:
            # ^^^^^^ last five bits of "b" are not all 1s, so this byte is last one
            # in tag block, so consider next byte as field length
            lengthByte = True
    dolData[1] = totalLength
    dolData.extend([0] * totalLength)

И вызовем инструкцию GET PROCESSING OPTIONS:

# Send command "GET PROCESSING OPTIONS"
#       CLA INS P1  P2  Lc      DATA
apdu = '80 A8   00  00  {:02X}  {}'.format(len(dolData), toHexString(dolData))
response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('GET PROCESSING OPTIONS failed')
    return 1

❈ ❈ ❈

Результат выполнения команды GET PROCESSING OPTIONS — это BER-TLV объект, в котором закодированы два значения: AIP и AFL. Если тег этого объекта 0x77, то они представлены как вложенные BER-TLV объекты. Если тег 0x80, то в виде байтовой строки.

  • AIP — Application Interchange Profile — определяет, какие функции поддерживает платёжное приложение на карте, размер этого блока данных — 2 байта.
  • AFL — Application File Locator — список файлов и записей, которые должно прочитать внешнее приложение.

Вот один пример результата команды GET PROCESSING OPTIONS.

80 0E 7C 00 08 01 01 00 10 01 05 00 18 01 02 01

Тег 0x80 говорит нам, что это примитивные данные, значит их структура следующая:

TagLengthValue
AIPAFL
800E7C 0008 01 01 00 10 01 05 00 18 01 02 01

Вот другой пример результата, здесь уже закодированные в BER-TLV эти же данные (тег 77):

77 12 82 02 7C 00 94 0C 08 01 01 00 10 01 05 00 18 01 02 01

В раскодированном виде они выглядят так:

TAG:   77(CONSTRUCTED)
  TAG:   82(PRIMITIVE)
  VALUE: 02 7C
  TAG:   94(PRIMITIVE)
  VALUE: 0C 08 01 01 00 10 01 05 00 18 01 02 01

Здесь в поле с тегом 0x82 хранится AIP, в поле с тегом 0x94 — AFL.

И вот код для обработки результата команды:

aipData = None
aflData = None
parts = bertlv.parse_bytes(response)
t = parts[0]
if t.tag == 0x77:
    x = bertlv.find_tag(0x82, t.value)
    if x is not None:
        aipData = x.value
    x = bertlv.find_tag(0x94, t.value)
    if x is not None:
        aflData = x.value
elif t.tag == 0x80:
    aipData = t.value[0:2]
    aflData = t.value[2:]

В AIP данные закодированы битами, их структура описана в Таблице 37 книги 3 EMV_v4.3. В этом фрагменте кода я вывожу на экран доступную в этом блоке данных информацию:

    print('Application Interchange Profile')
    print('  SDA supported: {}'.format('no' if (aipData[0] & 0x40)==0 else 'yes'))
    print('  DDA supported: {}'.format('no' if (aipData[0] & 0x20)==0 else 'yes'))
    print('  Cardholder verification is supported: {}'.format('no' if (aipData[0] & 0x10)==0 else 'yes'))
    print('  Terminal risk management is to be performed: {}'.format('no' if (aipData[0] & 0x8)==0 else 'yes'))
    print('  Issuer authentication is supported: {}'.format('no' if (aipData[0] & 0x4)==0 else 'yes'))
    print('  CDA supported: {}'.format('no' if (aipData[0] & 0x1)==0 else 'yes'))

Чтение данных платёжного приложения

Теперь мы готовы прочитать данные приложения, они хранятся в файлах, на которые ссылаются данные из AFL.

Структура AFL описана в разделе 10.2 книги 3 EMV_v4.3. Это список идущих подряд четвёрок байтов, внутри каждой четвёрки байты имеют следующую семантику:

  • пять старших битов (т.е. b8, b7, b6, b5, b4) первого байта задают Short File Identifier (SFI), оставшиеся три бита должны быть нулевыми;
  • второй байт задаёт номер первой записи, которую нужно прочитать из файла, определяемого SFI;
  • третий байт задаёт номер последней записи, которую нужно прочитать (включительно); он больше либо равен второму;
  • четвёртый байт определяет количество записей, вовлеченных в офлайновую аутентификацию данных (эта тема описана в книге 2 EMV_v4.3); эти записи начинаются с той, которая указана во втором байте; этот байт может быть нулевым.

ПО терминала обязано прочитать все указанные файлы и записи в том порядке, в каком они перечислены в AFL. Записи читаются командой READ RECORD из ISO/IEC 7816-4. Каждая запись представляет собой BER-TLV объект, теги для полей определены в EMV_v4.3. Клиентская программа должна данные из этих записей сохранить, чтобы использовать на следующих шагах.

Рассмотрим, например, значение AFL 28 01 03 01 30 01 02 00, оно состоит из двух четвёрок: 28 01 03 01 и 30 01 02 00. Рассмотрим первую из них подробно:

┌─────────── 28 = 0 0 1 0 1 0 0 0 → SFI = 0 0 1 0 1 = 5
│  ┌──────── номер первой записи для SFI
│  │  ┌───── номер последней записи для SFI
│  │  │  ┌── количество записей, необходимых для аутентификации карты
28 01 03 01

Это блок описывает три записи, в файле с SFI=05, для чтения этих записей нужно выполнить три команды с такими C-APDU (обратите внимание, что CLA=00, так как это межотраслевая инструкция из ISO/IEC 7816-4):

CLA INS P1 P2 Le
────────────────
00  B2  01 2C 00
00  B2  02 2C 00
00  B2  03 2C 00

В P1 передаётся номер записи, в нашем случае от 1 до 3, в P2 описывается, из какого файла. В битовой записи P2 в первых пяти битах (с b8 по b4) записывается SFI, а в последних трёх — 1 0 0, то есть прочитать только запись, указанную в P1. Аналогично разбирается вторая четвёрка.

❈ ❈ ❈

Разберём AFL на компоненты, распарсим их, прочитаем все записи из указанных файлов, получим на выходе плоский список BER-TLV объектов в переменной readObjects:

readObjects = []
aflPartsCount = len(aflData) // 4
for i in range(aflPartsCount):
    startByte = i * 4
    sfi = aflData[startByte] >> 3
    firstSfiRec = aflData[startByte + 1]
    lastSfiRec = aflData[startByte + 2]
    offlineAuthRecNumber = aflData[startByte + 3]  # we don't use this value

    #               CLA INS P1  P2  Le
    apdu = toBytes('00  B2  00  00  00')
    for j in range(firstSfiRec, lastSfiRec + 1):
        # set Le=0
        apdu[4] = 0
        # set P1, record number to read
        apdu[2] = j
        # set P2, coding of this parameters defined in ISO/IEC 7816-4, section "READ RECORD (S) command"
        p2 = (sfi << 3) | 4
        apdu[3] = p2
        response, sw1, sw2 = transmit_wrapper(cardservice.connection, apdu)
        if sw1 == 0x6C:
            # set new Le and repeat command
            apdu[4] = sw2
            response, sw1, sw2 = transmit_wrapper(cardservice.connection, apdu)
        if (sw1,sw2) != (0x90,0x00):
            print('Failed to read record {} in SFI {}'.format(j, sfi))
            continue
        parts = bertlv.parse_bytes(response)
        rectplTlv = bertlv.find_tag(0x70, parts)
        if rectplTlv is None:
            print('Failed to parse record {} in SFI {}'.format(j, sfi))
            continue
        for t in rectplTlv.value:
            readObjects.append(t)

Внутри каждой записи (определяемой SFI и номером записи) содержится TLV-объект с тегом 0x70, по сути это просто контейнер для хранения других TLV-объектов, мы их считываем и сохраняем в переменную readObjects, чтобы обработать позднее. Запрос с инструкцией B2 мы выполняем дважды: сначала для получения размера возвращаемого значения (указываем в Le=0), а потом уже с указанием конкретного значения Le, подобное мы уже делали в функции get_AID_from_PSEFCI.

❈ ❈ ❈

Теперь распечатаем в читабельном виде содержимое всех прочитанных объектов. Для этого нам нужно знать название для каждого из тегов и формат хранимых в нём данных. Для этих целей я сделал простой модуль emvutil.py с двумя функциями:

  • emv_object_name(tag) — человекочитаемое название элемента, идентифицируемого тегом tag
  • emv_object_repr(tag, value) — человекочитаемое представление данных элемента, идентифицируемого тегом tag

Последовательно проходим по всем запомненным элементам и печатаем их содержимое:

print('EMV objects:')
for t in readObjects:
    print('  {}: {}'.format( emvutil.emv_object_name(t.tag), emvutil.emv_object_repr(t.tag, t.value) ))

В модуле emvutil.py не очень много элементов, даже не все из спецификаций EMV (книга 3 EMV_v4.3), если какой-то элемент там не определён, то вместо имени выводится его представление как числа в HEX-нотации.

❈ ❈ ❈

Вот как выглядит результат работы программы (часть данных я сократил, часть скрыл):

% ./emv-card-read
Connected reader: ACS ACR 38U-CCID
Waiting for card ...
Card connected.
Application name: MasterCard
Application Priority Indicator: priority=1, confirmation required=False
Issuer Code Table Index: ISO 8859-1
Application Preferred Name: MasterCard
Language preference: ruenfrde
FCI Issuer Discretionary Data is present
Application Interchange Profile
  SDA supported: no
  DDA supported: yes
  Cardholder verification is supported: yes
  Terminal risk management is to be performed: yes
  Issuer authentication is supported: no
  CDA supported: no
EMV objects:
  Cardholder Verification Method (CVM) List: hex(00 00 00 00 00 00 00 00 42 01 1E 03 42 03 44 03 41 03 1F 03)
  Application Effective Date: 2011-09-01
  Application Expiration Date: 2021-02-28
  Application Usage Control: hex(FF 00)
  Application Primary Account Number (PAN): ???????????????? (hex(?? ?? ?? ?? ?? ?? ?? ??))
  Application Primary Account Number (PAN) Sequence Number: 2 (hex(02))
  Issuer Action Code - Default: hex(B8 50 BC 08 00)
  Issuer Action Code - Denial: hex(00 00 00 00 00)
  Issuer Action Code - Online: hex(B8 70 BC 98 00)
  Issuer Country Code (ISO 3166): 643 (hex(06 43))
  Static Data Authentication Tag List: hex(82)
  Card Risk Management Data Object List 1 (CDOL1): hex(9F 02 06 9F 03 06 .. 9F 7C 14)
  Card Risk Management Data Object List 2 (CDOL2): hex(91 0A 8A .. 37 04 9F 4C 08)
  Application Currency Code (ISO 4217): 643 (hex(06 43))
  Track 2 Equivalent Data: hex(?? ?? .. ??)
  Cardholder Name: STOLYAROV/SERGEY
  Application Version Number: hex(00 02)
  Application Currency Exponent: hex(02)
  Certification Authority Public Key Index: hex(05)
  Issuer Public Key Exponent: hex(03)
  Issuer Public Key Remainder: hex(B9 D0 9F EC 05 2C 8A B3 A1 .. 67 25 9A CA D7 FF FE 1E 0D 51 D3 91)
  Issuer Public Key Certificate: hex(74 BE C6 .. F5 F4 DC 76 5F 8D 55 DD)
  Dynamic Data Authentication Data Object List (DDOL): hex(9F 37 04)
  ICC Public Key Exponent: hex(03)
  ICC Public Key Certificate: hex(4F A0 26 65 DC .. D6 33 EE 7B 23 4D 93)

❈ ❈ ❈

И в завершение примера диаграмма, показывающая структуру файлов, как её видит считыватель:

EMV File System

  • DDFDirectory Definition File в терминологии EMV, является DF по ISO/IEC 7816
  • AEFApplication Elementary File в терминологии EMV, является EF по ISO/IEC 7816
  • PSEPayment System Environment, опциональный DDF, содержащий EF со списком установленных платёжных приложений
  • PSDPayment System Directory — это EF, содержащий имена платёжных приложений
  • ADFApplication Definition File в терминологии EMV, является DF по ISO/IEC 7816

Все DDF адресуются только по имени, адресация по идентификатору файла стандартом EMV не предусмотрена.

AEF внутри ADF содержат данные, необходимые для проведения транзакции, они адресуются по короткому идентификатору SFI, при этом номера с 1 по 10 выделены для данных, которые определены в EMV, номера с 11 по 20 выделены на усмотрение платёжной систему, а с 21 по 30 — конкретному банку, который выпустил карту.

Полностью структура файловой системы на EMV-карте описана в следующих разделах:

  • EMV_v4.3, книга 1, раздел 10 Files
  • EMV_v4.3, книга 3, раздел 5.3 Files

Теория: ATR

В этом разделе я подробно опишу структуру ATR. Как я уже раньше говорил, байтовый массив ATR (расшифровывается как Answer-To-Reset) посылается контактной картой при инициализации и содержит информацию, необходимую для корректной инициализации считывателя или стоящего за считывателем приложения. ATR для бесконтактных карт генерирует сам считыватель в рамках инфраструктуры PC/SC, чтобы для программы работа с картами выглядела единообразно.

Главная спецификация ATR для асинхронных карт описана в разделе 8.2 Answer-to-Reset стандарта ISO/IEC 7816-3, кроме того, дополнительные интерпретации есть и в других стандартах тоже, например, в EMV_v4.3, книга 1, раздел 8.2 Answer-to-Reset.

Логика построения ATR для синхронных карт (то есть карт памяти типа SLE5542) иная и задаётся в ISO/IEC 7816-10, однако в PC/SC выдаваемый считывателем в прикладную программу ATR кодируется специальным образом, чтобы соответствовать структуре ATR, описанной в ISO/IEC 7816-3 (и в этом тексте).

Я не буду рассказывать об электрической составляющей и сразу перейду к структуре.

ATR представляет собой байтовый массив размером от 2 до 33 включительно. В каждом байте кодируется одно или несколько именованных значений, а сами байты тоже имеют имена-обозначения:

ATR bytes

В начале идёт обязательный байт формата — T0, в нём закодированы два значения: индикатор Y1 и число K следующим образом:

ATR T0 coding

Интерфейсные байты TAi, TBi, TCi, TDi кодируют протоколы, которые предлагает карта, и параметры этих протоколов. Эти байты могут быть глобальными (global), тогда они касаются всех протоколов или каких-то параметров карты в целом; или специфичными для какого-то конкретного протокола.

TAi, TBi, TCi содержат данные, а TDi также определяет будут ли дальше в массиве следующие интерфейсные байты. Каждый байт TDi кодирует индикатор Yi+1 и тип T:

ATR TDi coding

Тип T определяет протокол, это число в диапазоне от 0 до 15 включительно, на данный момент только T=0 и T=1 являются допустимыми значениями. Заданный в TDi протокол использует параметры из TAi+1, TBi+1, TCi+1. Протоколы должны указываться по возрастанию номера. Если TD1 не существует, то подразумевается протокол T=0.

Четыре бита каждого индикатора Yi являются флагами, которые задают, какие интерфейсные байты последуют дальше. Если b5=1, то дальше идёт байт TAi, если b6=1, то байт TBi и так далее. Например, Y1=0101 означает, что дальше будут два байта TA1 и TС1, а других байтов не будет.

Очевидно, что при таком кодировании максимум может быть 32 блока: Y1 из T0 кодирует только TD1, Y2 из TD1 кодирует только TD2 и так далее до TD32, который кодирует Y33=0, так как больше байтов не допускается. Это вырожденная схема, но синтаксически она корректна.

Если TDi отсутствует, это сигнализирует об окончании блока интерфейсных байтов и дальше идут байты предыстории (historical bytes) в количестве K — это то самое число, которые было закодировано в T0.

❈ ❈ ❈

Семантика интерфейсных байтов весьма сложная и я её описывать здесь не буду, вместо этого рекомендую (в дополнение к ISO/IEC 7816-3) почитать статью в википедии (https://en.wikipedia.org/wiki/Answer_to_reset), где всё очень подробно расписано.

❈ ❈ ❈

Байты предыстории (historical bytes) — это дополнительные данные, помогающие идентифицировать карту. Детально структура этого блока описана в ISO/IEC 7816-4, раздел 12.2.2 Historical bytes.

Первый байт — это индикатор категории (category indicator), он может принимать следующие значения:

0x00
последующие данные состоят из закодированных в COMPACT-TLV объектов плюс в конце три обязательных байта индикатора состояния (status indicator, см. ISO/IEC 7816-4, раздел 12.2.2.11 Status indicator);
0x10
следующий байт является ссылкой на данные DIR (честно говоря, не имею ни малейшего представления, что это значит, оно имеет смысл для синхронных карт памяти, но там другая структура ATR, поэтому просто игнорирую);
0x80
последующие данные состоят из закодированных в COMPACT-TLV объектов, среди которых может находиться индикатор состояния (BER-TLV тег 0x48, компактный тег 0x81, 0x82 или 0x83);
0x81 - 0x8F
зарезервировано для будущего использования;
XX
все остальные значения означают проприетарный формат байтов предыстории, который не покрывается стандартом ISO/IEC 7816.

Упомянутый COMPACT-TLV — это способ кодирования межотраслевых BER-TLV объектов с тегом 4X:

  • если тег объекта — 0x4X;
  • если длина блока данных — 0x0Y;
  • то значение 0xXY называется компактным тегом, при этом сам COMPACT-TLV объект состоит из тега 0xXY и последующих Y байтов.

Обратное декодирование происходит аналогично.

Теги 0x4X — это теги класса приложения (то есть их семантика зафиксирована) и они определены в разных частях стандарта ISO/IEC 7816. Вот они все (RFU — reserved for future use, значение тега не определено на данный момент):

  • 0x40 — RFU
  • 0x41 — Country code and national data (ISO/IEC 7816-4, 12.2.2.3 Country or issuer indicator)
  • 0x42 — Issuer identification number (ISO/IEC 7816-4, 12.2.2.3 Country or issuer indicator)
  • 0x43 — Card service data (ISO/IEC 7816-4, 12.2.2.5 Card service data)
  • 0x44 — Initial access data (ISO/IEC 7816-4, 12.2.2.6 Initial access data)
  • 0x45 — Card issuer's data (ISO/IEC 7816-4, 12.2.2.7 Card issuer's data)
  • 0x46 — Pre-issuing data (ISO/IEC 7816-4, 12.2.2.8 Pre-issuing data)
  • 0x47 — Card capabilities (ISO/IEC 7816-4, 12.2.2.9 Card capabilities)
  • 0x48 — Status information (ISO/IEC 7816-4, 12.2.2.11 Status indicator)
  • 0x49 — RFU
  • 0x4A — RFU
  • 0x4B — RFU
  • 0x4C — RFU
  • 0x4D — Extended header list (ISO/IEC 7816-4, 8.4.5 Extended header and extended header list)
  • 0x4E — RFU
  • 0x4F — Application identifier (ISO/IEC 7816-4, 12.2.2.4 Application identifier)

Подробную семантику этих объектов вы можете найти либо в указанных частях стандарта, либо в примере ниже.

❈ ❈ ❈

Байт TCK является опциональным и служит для проверки целостности ATR, он содержит XOR всех байтов, начиная с T0 и заканчивая байтом перед TCK.

В PC/SC первым байтом ATR является не T0, а TS, он определён в ISO/IEC 7816-3 и задаёт электрические/сигнальные параметры коммуникации. Несмотря на то, что в стандарте TS не включён в ATR, PC/SC всё равно его возвращает. Это нужно учитывать при разборе.

example-13: Разбор ATR

Исходный код программы: example-13/atr-parse

Для этого примера можно использовать оба типа считывателей: для бесконтактных и контактных чиповых карт.

Практической ценности этот пример не представляет, поскольку ATR предназначен для считывателя, который его использует для установки и настройки соединения. Но я всё же его включил в текст, чтобы совместить описание с кодом для более продуктивного изучения.

Сгруппированные байты интерфейса будем хранить в отдельных объектах класса InterfaceBytes.

Начинаем с разбора байта формата:

# check format byte T0
T0 = data[1]
Y = (T0 >> 4) & 0xF;
historicalBytesLength = T0 & 0xF

Мы используем одну переменную для хранения текущего Yi, поскольку нет необходимости хранить их все.

Далее читаем все интерфейсные байты с записью их в список allInterfaceBytes, в конце цикла обновляем переменные T и Y новыми значениями:

# read interface bytes
p = 2
T = 0
while True:
    TA = None
    TB = None
    TC = None
    TD = None
    # check is next byte is TAi
    if (Y & 1) != 0:
        TA = data[p]
        p += 1
    # check is next byte is TBi
    if (Y & 2) != 0:
        TB = data[p];
        p += 1
    # check is next byte is TCi
    if (Y & 4) != 0:
        TC = data[p]
        p += 1
    # check is next byte is TDi
    if (Y & 8) != 0:
        TD = data[p]
        p += 1
    allInterfaceBytes.append(InterfaceBytes(TA, TB, TC, TD, T))

    if TD is None:
        break

    T = TD & 0xF
    Y = (TD >> 4) & 0xF

Выделяем байты предыстории:

historicalBytes = data[p : p + historicalBytesLength]

На байт проверки TCK забиваем и не проверяем его.

❈ ❈ ❈

Теперь можно распечатать данные, начнём с интерфейсных байтов:

# print interface bytes
print('Interface bytes:')
for i,b in enumerate(allInterfaceBytes, 1):
    if b.TA is not None:
        print('  TA{} = {:02X} (T = {})'.format(i, b.TA, b.T))
    if b.TB is not None:
        print('  TB{} = {:02X} (T = {})'.format(i, b.TB, b.T))
    if b.TC is not None:
        print('  TC{} = {:02X} (T = {})'.format(i, b.TC, b.T))
    if b.TD is not None:
        print('  TD{} = {:02X} (T = {})'.format(i, b.TD, b.T))

С точки зрения внешнего приложения ничего особо интересного в интерфейсных байтах нет, там содержатся только данные для низкоуровневого протокола. А вот байты предыстории я постараюсь разобрать подробнее, так как там встречается полезная информация.

В ISO/IEC 7816-4, в разделе 12.2.2 Historical bytes описываются правила разбора байтов предыстории. У себя в коде я это делаю в функциях printHistoricalBytesValue, getStatusIndicatorBytes, getCapabilities (их код я тут не привожу, так как он очень объёмный, смотрите файл atr-parse).

print('Historical bytes length (K): {}'.format(historicalBytesLength))
print('Historical bytes (raw): {}'.format(toHexString(historicalBytes)))

if historicalBytes[0] == 0x80:
    # parse all as COMPACT-TLV objects
    p = 1
    while True:
        if p == historicalBytesLength:
            break
        if p > historicalBytesLength:
            raise('Incorrect historical bytes structure.')
        objLen = historicalBytes[p] & 0xF
        objTag = ((historicalBytes[p] >> 4) & 0xF) + 0x40
        objData = historicalBytes[p + 1 : p + objLen + 1]
        printHistoricalBytesValue(objTag, objData)
        p += objLen + 1

elif historicalBytes[0] == 0x0:
    pass
else:
    print('Proprietary historical bytes structure.');

❈ ❈ ❈

SIM-карта сотовой связи МТС:

% ./atr-parse
Connected reader: ACS ACR 38U-CCID
Waiting for the card...
Inserted card with ATR: 3B 9F 95 80 1F 47 80 31 E0 73 36 21 13 57 4A 33 0E 10 31 41 00 B4
Interface bytes:
  TA1 = 95 (T = 0)
  TD1 = 80 (T = 0)
  TD2 = 1F (T = 0)
  TA3 = 47 (T = 15)
Historical bytes length (K): 15
Historical bytes (raw): 80 31 E0 73 36 21 13 57 4A 33 0E 10 31 41 00
  TAG: 43; DATA: E0
    Card service data:
      Application selection by full DF name: yes
      Application selection by partial DF name: yes
      BER-TLV data objects in EF.DIR: yes
      BER-TLV data objects in EF.ATR: no
      EF.DIR and EF.ATR access services: by the READ RECORD (S) command (record structure)
      Card with MF
  TAG: 47; DATA: 36 21 13
    Card capabilities
      DF selection: by path, by file identifier
      Short EF identifier supported: yes
      Record number supported: yes
      Record identifier supported: no
      EFs of TLV structure supported: no
      Behaviour of write functions: Proprietary
      Value 'FF' for the first byte of BER-TLV tag fields: invalid
      Commands chaining: no
      Extended Lc and Le fields: no
      Logical channel number assignment: by the card
      Maximum number of logical channels: 4
  TAG: 45; DATA: 4A 33 0E 10 31 41 00
    Card issuer's data

SIM-карта сотовой связи beeline:

% ./atr-parse
Connected reader: ACS ACR 38U-CCID
Waiting for the card...
Inserted card with ATR: 3B 9F 94 80 1F C7 80 31 E0 73 FE 21 1B 57 3C 86 60 CD A1 00 12 46
Interface bytes:
  TA1 = 94 (T = 0)
  TD1 = 80 (T = 0)
  TD2 = 1F (T = 0)
  TA3 = C7 (T = 15)
Historical bytes length (K): 15
Historical bytes (raw): 80 31 E0 73 FE 21 1B 57 3C 86 60 CD A1 00 12
  TAG: 43; DATA: E0
    Card service data:
      Application selection by full DF name: yes
      Application selection by partial DF name: yes
      BER-TLV data objects in EF.DIR: yes
      BER-TLV data objects in EF.ATR: no
      EF.DIR and EF.ATR access services: by the READ RECORD (S) command (record structure)
      Card with MF
  TAG: 47; DATA: FE 21 1B
    Card capabilities
      DF selection: by full DF name, by partial DF name, by path, by file identifier, Implicit DF selection
      Short EF identifier supported: yes
      Record number supported: yes
      Record identifier supported: no
      EFs of TLV structure supported: no
      Behaviour of write functions: Proprietary
      Value 'FF' for the first byte of BER-TLV tag fields: invalid
      Commands chaining: no
      Extended Lc and Le fields: no
      Logical channel number assignment: by the interface device
      Maximum number of logical channels: 4
  TAG: 45; DATA: 3C 86 60 CD A1 00 12
    Card issuer's data

Карта МИР:

% ./atr-parse
Connected reader: ACS ACR 38U-CCID
Waiting for the card...
Inserted card with ATR: 3B 78 13 00 00 80 31 C0 72 F7 41 81 07
Interface bytes:
  TA1 = 13 (T = 0)
  TB1 = 00 (T = 0)
  TC1 = 00 (T = 0)
Historical bytes length (K): 8
Historical bytes (raw): 80 31 C0 72 F7 41 81 07
  TAG: 43; DATA: C0
    Card service data:
      Application selection by full DF name: yes
      Application selection by partial DF name: yes
      BER-TLV data objects in EF.DIR: no
      BER-TLV data objects in EF.ATR: no
      EF.DIR and EF.ATR access services: by the READ RECORD (S) command (record structure)
      Card with MF
  TAG: 47; DATA: F7 41
    Card capabilities
      DF selection: by full DF name, by partial DF name, by path, by file identifier
      Short EF identifier supported: yes
      Record number supported: yes
      Record identifier supported: yes
      EFs of TLV structure supported: no
      Behaviour of write functions: Write OR
      Value 'FF' for the first byte of BER-TLV tag fields: invalid
  TAG: 48; DATA: 07
    Status information:
      LCS (life cycle status): Operational state (activated)

Карта спутникового телевидения НТВ+:

% ./atr-parse
Connected reader: ACS ACR 38U-CCID
Waiting for the card...
Inserted card with ATR: 3F 77 18 00 00 C2 EB 41 02 6C 90 00
Interface bytes:
  TA1 = 18 (T = 0)
  TB1 = 00 (T = 0)
  TC1 = 00 (T = 0)
Historical bytes length (K): 7
Historical bytes (raw): C2 EB 41 02 6C 90 00
Proprietary historical bytes structure.

Использование файловой системы ISO/IEC 7816-4 на примере UICC

В сотовых телефонах и модемах для хранения информации о пользователе (идентификатор и секретный ключ) используются смарт-карты, которые обычно называют SIM-картам, SIM — это сокращение от Subscriber Identification Module. Однако со временем внутреннее устройство этих карт существенно изменилось и расширилось, и теперь они служат не только для хранения данных, но и частично для их защищённой обработки. Впоследствии вместо SIM-карты как технического устройства стал использоваться термин UICC — Universal integrated circuit card, однако неформально UICC по-прежнему назвают SIM-картами.

Электрически и программно UICC/SIM карты удовлетворяют семейству стандартов ISO/IEC 7816, однако непосредственно для полной спецификации существует специализированное семейство стандартов, развиваемое ETSI (European Telecommunications Standards Institute) и сообществом 3GPP (3rd Generation Partnership Project). Версий и вариантов стандартов очень много, они выпускаются достаточно часто и помимо стандартизации UICC также отвечают за всю область сотовой связи в целом. В этой статье я буду ссылать на конкретные версии спецификаций в виде ссылок на PDF-документы. Все спецификации свободно доступны на сайте https://www.3gpp.org/specifications-technologies/specifications-by-series.

UICC-карты удовлетворяют стандартам ISO/IEC 7816 и поэтому к ним можно применять те же алгоритмы и способы исследования, что и для EMV-карт, например. Однако физический формат UICC не позволяет использовать их напрямую в стандартном считывателе, поэтому вам придётся сделать специальный адаптер из «большой» сим-карты, из которой обычно выламывается обычная, micro или nano-сим карта. Идею вы должны понять из фотографии:

SIM to ID-1 adapter

Чтобы карта держалась, можно с обратной стороны приклетить кусок скотча. Для nano SIM нужно взять дополнительно соответствующий адаптер.

⭐ Я далее буду иногда вместо UICC использовать слово SIM, но в данной статье именно классическая SIM-карта не подойдёт, поскольку UICC гораздо мощнее и функциональнее старых SIM-карты, которые по сути были простыми картами памяти. Многие из примеров ниже просто не будут работать на старых SIM-картах, так как они используют более ранние версии спецификаций.

❈ ❈ ❈

Интересующий нас документ стандарта называется ETSI TS 102 221 Release 15, Smart cards; UICC-Terminal interface; Physical and logical characteristics, его можно свободно скачать: https://www.etsi.org/deliver/etsi_ts/102200_102299/102221/15.00.00_60/ts_102221v150000p.pdf (Release 15 на момент написания статьи). В нём описываются физические и коммуникационные аспекты UICC, в частности, форматы APDU и элементов файловой системы ISO/IEC 7816. В дальнейшем я буду ссылаться на конкретные релизы спецификаций, даже если в момент чтения вышли более новые версии.

В разделе 8.6 Reservation of file Ids спецификации ETSI TS 102 221 описаны зарезервированные идентификаторы информационных объектов, которые могут присутствовать на UICC. А в документе ETSI TS 131 122 Release 17 есть их иерархичная схема:

UICC reseved IDs

Во всех этих объектах хранится информация, необходимая для функционирования модуля сотовой связи. Если интересно, можете ознакомиться с неплохой обзорной презентацией SIM/USIM cards.

MF с идентификатором 0x3F00 описан в ISO/IEC 7816-4, это назначенный файл (dedicated file, DF), представляющий корень иерархии всех объектов, у него есть зарезервированное название — Master File или MF, неформально его ещё называют корневым каталогом (root directory). Его особенность в том, что он выбирается (инструкцией SELECT) автоматически после инициализации карты в считывателе.

В UICC есть несколько элементарных файлов (EF), в которых хранятся собственно данные. Доступ к этим файлам (как на чтение, так и на запись) может ограничиваться PIN-кодом. Элементарные файлы на схеме выше отмечены одинарной рамкой, а каталоги — двойной.

❈ ❈ ❈

Схема формирования C-APDU описана в разделе 10.1 Command APDU ETSI TS 102 221 Release 15. Для совместимости новые UICC предоставляют интерфейс как у старых SIM-карт, для них байт класса CLA равен A0, однако мы этим не пользуемся и у нас везде байт класса равен 00.

❈ ❈ ❈

При использовании команды SELECT (инструкция A4) можно указать, какую информацию о файле мы хотим получить в ответе, всего есть три варианта: File Control Information (FCI), File Control Parameters (FCP), File Management Data (FMD). Для каждого из них задан свой шаблон (template) с выделенным тегом для идентификации шаблона. Полностью команда SELECT в применении к UICC описана в ETSI TS 102 221 Release 15, раздел 11.1.1 SELECT.

Для UICC используется только FCP Template, поэтому C-APDU выбора, скажем, MF будет выглядеть так:

CLA  INS  P1  P2  Lc  DATA
00   A4   00  04  02  3F 00

В аргументе P2 мы указываем 0x04, в битовом виде это 0 0 0 0 1 0 0, смысл битов описан в разделе Table 11.2: Coding of P2 ETSI TS 102 221 Release 15.

В блоке DATA указываем идентификатор MF — 0x3F00.

Результатом этой команды будет блок данных с закодированным FCP Template. Это BER-TLV объект с тегом 0x62, он содержит элементы данных как межотраслевые (определённые в ISO/IEC 7816-4, раздел 7.4.3 Control parameters), так и специфичные именно для UICC (они описаны в разделе 11.1.1 SELECT документа ETSI TS 102 221 Release 15).

⭐ В случае использовании протокола T=0 для получения данных понадобится отдельная команда GET RESPONSE, которую я описывал выше в разделе Обработка результата выполнения команды в протоколе T=0. Для совсем старых SIM-карт вместо статусного слова 61 XX использовалось 9F XX.

Вот пример разбора результата команды SELECT на MF (FCP Template):

62 26 82 02 78 21 83 02 3F 00 A5 03 80 01 71 8A 01 05 8B 03 2F 06 0D C6 0F 90 01 70 83 01 01 83 01 81 83 01 0A 83 01 0B

0x62  // FCP Template
  0x82: (RAW) 78 21  // File Descriptor
  0x83: (RAW) 3F 00  // File Identifier
  0xA5               // Proprietery information
    0x80: (RAW) 71   // UICC characteristics, see [ETSI TS 102 221 Release 15], section 11.1.1.4.6.1 
  0x8A: (RAW) 05     // Life Cycle Status Integer
  0x8B: (RAW) 2F 06 0D  // Security attributes
  0xC6: (RAW) 90 01 70 83 01 01 83 01 81 83 01 0A 83 01 0B  // PIN Status Template Data Object

Детальное описание каждого из полей в документе ETSI TS 102 221 Release 15.

❈ ❈ ❈

Другим важным файлом является EF с идентификатором 0x2F00, его также ещё называют EF.DIR. В этом файле с линейной структурой записей перечислены все доступные приложения на карте. Выше в разделе Разбор FCI template (get_AID_from_PSEFCI) я показывал, как читаются записи из EF командой READ RECORD. А конкретно для UICC будет пример в следующем разделе.

Для EF тоже можно получить данные в виде FCP Template, делается это аналогично через C-APDU:

00 A4 00 04 02 2F 00

Результат команды (возможные объекты перечислены в разделе 11.1.1.3.2 Response for an EF ETSI TS 102 221 Release 15):

62 1A 82 05 42 21 00 20 01 83 02 2F 00 8A 01 05 8B 03 2F 06 07 80 02 00 20 88 01 F0

0x62
  0x82: (RAW) 42 21 00 20 01  // File Descriptor
  0x83: (RAW) 2F 00           // File Identifier
  0x8A: (RAW) 05              // Life Cycle Status Integer
  0x8B: (RAW) 2F 06 07        // Security attributes
  0x80: (RAW) 00 20           // File size
  0x88: (RAW) F0              // Short File Identifier (SFI)

❈ ❈ ❈

И для примера ещё один обязательный объект EFICCID, который находится непосредственно под MF и имеет фиксированный идентификатор 0x2FE2. Этот файл имеет прозрачную (transparent) структуру, читается командой READ BINARY и содержит уникальный идентификатор UICC длиной 10 байтов. Этот EF описан в разделе 13.2 EFICCID (ICC Identification) ETSI TS 102 221 Release 15.

После выделения этого файла командой 00 A4 00 00 02 2F E2 вызывается READ BINARY (см. раздел 11.1.3 READ BINARY ETSI TS 102 221 Release 15) 00 B0 00 00 0A (листинг работы программы apdu-terminal из примера выше):

APDU% 00 A4 00 00 02 2F E2
> 00 A4 00 00 02 2F E2
< [empty response] Status: 90 00
APDU% 00 B0 00 00 0A
> 00 B0 00 00 0A
< 98 07 91 29 00 38 96 15 17 F8 Status: 90 00

Обратите внимание, что мы при выделении файла не запрашивали данные шаблона FCP (P1=0x00, P2=0x00 вместо P2=0x04), поэтому в ответ получили статусное слово 0x9000 и пустой блок данных.

В объекте EFICCID хранится уникальный номер UICC, он также напечатан на каждой симке, на обратной стороне, где нет контактов, в виде группы цифр в несколько строк. Также на «большой» карте ID-1 этот номер напечатан в виде штрих-кода. В EFICCID номер закодирован как Binary Coded Decimal с левым выравниваем и разделителем 0xF, поэтому значение 98 07 91 29 00 38 96 15 17 F8 соответствует номеру 897019920083695171.

example-14: Чтение данных с UICC

Исходный код программы: example-14/uicc-read

Для этого примера можно использовать только считыватель контактных чиповых карт плюс специальный адаптер, чтобы «дотянуться» контактами UICC-симки до контактов считывателя.

Цели этого примера:

  • продемонстрировать на примере UICC работу с файловой системой ISO/IEC 7816 и различными типами информационных объектов;
  • разобрать и отобразить в читабельном виде данные с UICC.

Для начала мы выбираем и читаем FCP для MF:

# select MF and fetch FCP
#       CLA INS P1  P2  Lc  DATA
apdu = '00  A4  00  04  02  3F 00'
response, sw1, sw2 = transmit_wrapper(cardservice.connection, toBytes(apdu))
if (sw1,sw2) != (0x90,0x00):
    print('Failed to read MF FCP Template')
    return 1

parts = bertlv.parse_bytes(response)
mf_fcp = bertlv.find_tag(0x62, parts)
if mf_fcp is None:
    print('Failed to parse MF FCP Template')
    return 1

print('MF FCP data:')
for tlv in mf_fcp.value:
    if tlv.tag == 0x82:
        print('  File descriptor: {}'.format(toHexString(tlv.value)))
    elif tlv.tag == 0x83:
        print('  File identifier: {}'.format(toHexString(tlv.value)))
    elif tlv.tag == 0x84:
        print('  DF name (AID): {}'.format(toHexString(tlv.value)))
    elif tlv.tag == 0xA5:
        print('  Proprietary information:')
        for x in tlv.value:
            print('    0x{:X}: {}'.format(x.tag, toHexString(x.raw_value)))
    elif tlv.tag == 0x8A:
        print('  Life Cycle Status Integer: {}'.format(toHexString(tlv.value)))
    elif tlv.tag in (0x8B, 0x8C, 0xAB):
        print('  Security attributes: {}'.format(toHexString(tlv.value)))
    elif tlv.tag == 0xC6:
        print('  PIN Status Template DO: {}'.format(toHexString(tlv.value)))
    elif tlv.tag == 0x81:
        print('  Total file size: {}'.format(toHexString(tlv.value)))
    else:
        print('  0x{X}: {}'.format(tlv.tag, tlv.raw_value))

Структура всех информационных объектов описана в разделе 11.1.1 SELECT ETSI TS 102 221 Release 15. Например, для объекта с тегом 0x82 (File descriptor) заданы правила интерпретации байтов, среди которых хранится, например, размер и количество записей для элементарного (EF) линейного файла с записями.

Далее мы выбираем элементарный файл EF.DIR, в котором записаны все приложения на карте, чтение FCP примерно такое же, поэтому этот код тут не привожу.

Дальше читаем записи из EF.DIR.

# read records
efdir_records = []
while True:
    #                    CLA INS P1  P2  Lc
    apduBytes = toBytes('00  B2  00  02  00')
    response, sw1, sw2 = transmit_wrapper(cardservice.connection, apduBytes)
    if sw1 == 0x6C:
        # set new Le and repeat command
        apduBytes[4] = sw2
        response, sw1, sw2 = transmit_wrapper(cardservice.connection, apduBytes)
    if (sw1,sw2) == (0x6A,0x83):
        break
    elif (sw1,sw2) != (0x90,0x00):
        print('Failed to read record')
        break
    parts = bertlv.parse_bytes(response)
    app_record = bertlv.find_tag(0x61, parts)
    if app_record is not None:
        efdir_records.append(app_record)

Записи из текущего выбранного файла мы читаем командой READ RECORD с кодом инструкции 0xB2, в аргументе P1 указываем 0x00 (что означает, что мы хотим прочитать не конкретную запись, а последовательно все), в P2 указываем значение 0x02, которое означает, что при каждом вызове мы должны прочитать следующую запись. При первом вызове для каждой записи передаём в поле Lc 0x00, чтобы в статусном слове 6A LL вернулась длина записи 0xLL, эту длину далее указываем в Lc и получаем фактические байты.

В сыром виде запись выглядит примерно так:

61 14 4F 0C A0 00 00 00 87 10 02 FF FF FF FF 89 50 04 55 53 49 4D FF FF FF FF FF FF FF FF FF FF

Длина каждой записи для этого файла фиксированная, поэтому последние байты добиваются значением 0xFF. Информация о том, что 0xFF используется для заполнения пустого места записана в поле File descriptor FCP, она там закодирована в виде особого бита в байте Data coding byte. Также в File descriptor хранится тип записей в файле (если там вообще записи), их количество и максимальная длина. Мы должны это всё анализировать для корректного использования, но в примере я этого не делаю.

Далее мы печатаем содержимое этих записей — это всё BER-TLV объекты, в каждом из которых записан идентификатор приложения и его текстовая метка.

print('EF.DIR records:')
for i,tlv in enumerate(efdir_records, 1):
    print(f'Record {i}')
    for x in tlv.value:
        if x.tag == 0x4F:
            print('  AID: {}'.format(toHexString(x.value)))
        elif x.tag == 0x50:
            print('  Application label: {}'.format(HexListToBinString(x.value)))
        else:
            print('  0x{:X}: {}'.format(x.tag, toHexString(x.raw_value)))

❈ ❈ ❈

Результат работы программы выглядит примерно так на примере карты beeline:

% ./uicc-read
Connected reader: ACS ACR 38U-CCID
Waiting for card ...
Card connected.
MF FCP data:
  File descriptor: 78 21
  File identifier: 3F 00
  Proprietary information:
    0x80: 71
  Life Cycle Status Integer: 05
  Security attributes: 2F 06 0D
  PIN Status Template DO: 90 01 70 83 01 01 83 01 81 83 01 0A 83 01 0B

EF.DIR:
  File descriptor: 42 21 00 20 01
  File identifier: 2F 00
  Life Cycle Status Integer: 05
  Security attributes: 2F 06 07
  File size: 00 20
  Short file identifier: F0
EF.DIR records:
Record 1
  AID: A0 00 00 00 87 10 02 FF FF FF FF 89
  Application label: USIM

И вот так на примере карты Тинкофф Мобайл (здесь уже два приложения в EF.DIR):

% ./uicc-read
Connected reader: ACS ACR 38U-CCID
Waiting for card ...
Card connected.
MF FCP data:
  File descriptor: 78 21
  File identifier: 3F 00
  Proprietary information:
    0x80: 61
    0x87: 01
    0x83: 00 06 15 00
  Life Cycle Status Integer: 05
  Security attributes: 2F 06 01 0B 00 0B
  PIN Status Template DO: 90 01 40 83 01 01 83 01 0A
  Total file size: FF FF

EF.DIR:
  File descriptor: 42 21 00 40 02
  File identifier: 2F 00
  Proprietary information:
    0xC0: 40
  Life Cycle Status Integer: 05
  Security attributes: 2F 06 01 03 00 03
  File size: 00 80
  Total file size: 00 94
  Short file identifier: F0
EF.DIR records:
Record 1
  AID: A0 00 00 00 87 10 02 FF FF F0 01 89 00 00 01 FF
  Application label: USIM
  0x73: A0 10 80 03 17 12 12 81 04 5F 40 5F 30 82 03 45 41 50
Record 2
  AID: A0 00 00 00 87 10 04 FF FF F0 01 89 00 00 01 FF
  Application label: ISIM

❈ ❈ ❈

Для детального изучения содержимого UICC-карты я рекомендую проект pySim, это написанный на питоне набор инструментов для тщательного изучения и модификации SIM и UICC карт. В частности там есть интерактивная оболочка, позволяющая удобно «ходить» по структуре файлов UICC смарт-карты.

Заключение

На этом я статью хочу завершить и подвести итоги. Главной моей целью был обзор технологий и стандартов для практического использования смарт-карт. Самые главные моменты, которые вы постарался донести:

  • PC/SC как главная коммуникационная библиотека и интерфейс к ней в виде библиотеки pyscard;
  • APDU как способ коммуникации с картой из внешнего приложения;
  • основные отраслевые стандарты;
  • принципы работы с данными на смарт-карте;
  • кодирование объектов (TLV, SIMPLE-TLV, BER-TLV).

Ссылки

Даташиты и руководства

Стандарты и спецификации

Литература

  • Голдовский И.М. Банковские микропроцессорные карты // ISBN 978-5-9614-1233-8 — великолепная книга, настоятельно её рекомендую, в ней детально описываются технологии и способы работы с банковскими картами.
  • Uwe Hansmann, Martin S. Nicklous, etc Smart Card Application Development Using Java — книга о том, как писать приложения для смарт-карт с использование OpenCard Framework.
  • Ugo Chirico Smart Card Programming, книга примерно о том же, о чём эта статья, только гораздо детальнее и на английском
  • Wolfgang Rankl, Wolfgang Effing Smart Card Handbook — очень подробная книга на тысячу страниц о технологиях смарт-карт, протоколах, криптографии и всех остальных аспектах, практически энциклопедия
  • Голдовский И.М., etc Бизнес-энциклопедия «Платёжные карты» // ISBN 978-5-406-03339-5 — книга об истории (в том числе российской) банковских карт

Инструменты, сайты и ПО

  • EMV TLV Parser — удобный онлайновый парсер BER-TLV данных.
  • ACS Script Tool — десктопная программа от ACS для отправки APDU в считыватель и автоматизации этого процесса, есть версии под Windows, Linux и Macos.
  • EFTlab — большая база знаний по EMV: коды и названия объектов, идентификаторов и других информационных элементов.
  • Smart card ATR parsing — разбор ATR онлайн.
  • ASN.1 Complete — полное руководство по ASN.1
  • pySim — набор инструментов для просмотра и редактирования UICC/SIM карт и документация к нему

Комментарии

Текст комментария (допустимая разметка: *курсив*, **полужирная**, [ссылка](http://example.com) или <http://example.com>) Посетители-анонимы, обратите внимение, что более чем одна гиперссылка в тексте (включая оную из поля «веб-сайт») приведёт к блокировке комментария для модерации. Зайдите на сайта с использованием аккаунта на twitter, например, чтобы посылать комментарии без этого ограничения.
Имя (обязательно, 50 символов или меньше)
Опциональный email, на который получать ответы (не будет опубликован)
Веб-сайт
© 2006—2023 Sergey Stolyarov | Работает на pyrengine