В 2017 году я написал статью Смарт-карты и программирование с примерами кода C/C++. Однако для обучения этот язык подходит плохо, поскольку предполагает достаточно низкоуровневое и многословное использование функций и данных, поэтому я решил сначала переписать все примеры на Java, однако в процессе пришлось значительно переосмыслить старый текст, обновить и переработать с учётом накопившегося опыта, ошибок и замечаний.
Статья 2017 «заморожена» и я не планирую её больше обновлять, однако вы можете её читать, если хотите понять, как всё работает на сравнительно низком уровне, где вручную нужно выделять и освобождать ресурсы, инициализировать буферы и так далее.
Это очень большой текст о смарт-картах и о том, как писать клиентский софт на Java для их использования. От вас требуется только знание Java, понимание базовых алгоритмов, структур данных и, собственно, смарткарты с терминалами. Все примеры ориентированы на unix-окружение, в первую очередь это linux и mac os x. Мобильные операционные системы не рассматриваются. Код, вероятно, работает в windows-окружении, однако я это не тестировал. Все примеры полностью независимы друг от друга и не используют никакие сторонние библиотеки или общие файлы.
Язык Java я выбрал по нескольким причинам. Во-первых, он достаточно высокого уровня и вы избавлены от ручного управления памятью. Во-вторых, в Java очень богатая стандартная библиотека. В третьих, Java как платформа работает на разных операционных системах практически одинаково, что очень упрощает процесс обучения. И самое главное, в штатной поставке платформы уже есть поддержка библиотеки для работы со смарт-картами. Я выбрал современную базовую версию SDK — 11 — и все примеры кода написаны с учётом работающих в этой версии фич (типа использования ключевого слова var
для автоматического вывода типов).
❈ ❈ ❈
Для выполнения всех примеров кода вам нужен USB-терминал (ридер) для смарт-карт. В некоторых примерах можно использовать USB крипто-токен (eToken, yubico и т.п.), они используют тот же программный интерфейс.
Ещё в статье очень много новых англоязычных терминов, сокращений, ссылок на международные стандарты и спецификации. Слишком глубоко в детали я вдаваться не стану, но буду ссылаться на внешние документы. Очень желательно как минимум понимать английский, так как все оригинальные стандарты и спецификации написаны на нём.
Для более глубокого понимания вам нужно обязательно иметь под рукой упоминаемые стандарты. К сожалению, стандарты ISO/IEC недоступны для свободного скачивания, однако их при желании можно найти в интернете. Их официальные русские переводы (ГОСТ), однако, свободно доступны и ссылки на них вы найдёте в тексте.
Другие стандарты доступны для свободного скачивания и я тоже дам ссылки на них.
❈ ❈ ❈
- Что такое смарт-карта
- Оборудование и средства разработки
- Отладка в Macos
- Отладка в linux
- Программные библиотеки
- example-01: инициализация библиотеки
- example-02: подключение к терминалу
- Теория: архитектура PC/SC
- example-03: Получение UID бесконтактной карты
- Теория: данные на смарт-карте
- example-04: Чтение карты Mifare Classic
- example-05: Разбор данных на карте Mifare Classic
- example-06: Персонализация карты Mifare Classic 1K
- Работа с контактными картами памяти
- example-07: Работа с контактной картой памяти SLE 5542
- example-08: Приложение для отправки APDU
- Теория: отраслевые стандарты
- Теория: взаимодействие с микропроцессорной картой
- Теория: кодирование информационных объектов
- example-09: Чтение данных на примере банковской карты
- Теория: ATR
- example-10: Разбор ATR
- Заключение
Что такое смарт-карта¶
Изначально смарт-карта представляла собой пластиковую карту формата ID-1 размером 85,60 × 53,98 мм со скруглёнными углами (стандартная банковская/кредитная карточка имеет такую же форму и размеры). В неё вмонтирован микрочип, коммуникационные контакты которого выведены на одну из сторон.
Позднее появились смарт-карты в формате ID-000, они стали использоваться в мобильных телефонах и нам известны как SIM-карты. До сих пор операторы сотовой связи продают SIM-карты в формате ID-1, из которого ID-000 можно выломать и вставить в телефон.
Дальше появились смарт-карты без внешней контактной площадки, а для коммуникации и передачи питания стали использовать радиоканал. Также API для работы со смарт-картами используют некоторые USB-устройства, например, крипто-токены (Yubikey, eToken, Aladdin итп).
Смарт-карты не имеют собственного источника питания и полагаются на внешнее.
Основным преимуществом смарт-карты является физическая безопасность размещённых на ней данных. Микрочип очень маленький и всё «оборудование» микрокомпьютера размещено на нём, без вывода наружу внутренних контактов, к которым можно подключиться для перехвата. Также карты разрабатывались с учётом других возможных атак: сканирования рентгеновскими лучами, микрошлифования и так далее.
Примерный жизненный цикл смарт-карты (например, банковской карты с чипом) состоит из нескольких этапов. В нём участвуют производитель чипа, производитель смарт-карты, заказчик и клиент:
- Изготовление чипа/процессора. На этой стадии непосредственный изготовитель чипа после физического производства также записывает на него предоставленную производителем смарт-карт универсальную и одинаковую для всех карт партии «прошивку».
- Первичная инициализация смарт-карты. Партия чипов отправляется производителю карт, он модифицирует прошивку необходимым образом, например, прописывает внутрь каждой карты уникальный серийный номер, после чего специальным запросом отключает возможность менять его.
- Изготовление смарт-карты. Производитель вставляет чип в карты нужного формата и отправляет их заказчику, например, банку.
- Персонализация. Заказчик, используя методы прошитого ПО на карте, записывает на неё свои приложения, например, банковские; а также дополнительные данные, например, имя клиента, номер счёта и так далее. После чего специальным запросом финализирует карту, после чего ограничивается запись новых приложений, например.
- Выдача. Карта отдаётся клиенту и клиент её использует.
- Уничтожение. Карта выбрасывается.
Главным стандартом для смарт-карт является ISO/IEC 7816, он охватывает все аспекты устройства, коммуникации и эксплуатации карт с контактной площадкой (контактных карт).
Для бесконтактных смарт-карт используется стандарт ISO/IEC 14443, он описывает детали, специфичные именно для устройств с радио-интерфейсом. Позднее он вошёл в более широкую группу, куда также входят стандарты для NFC.
Существует несколько других стандартов, о некоторых из них я расскажу позднее. Сразу отмечу, что я не буду рассказывать о физических и низкоуровневых деталях функционирования смарт-карт, нас интересуют только сравнительно высокоуровневые операции.
Оборудование и средства разработки¶
Статья полностью посвящена работе с чиповыми картами на десктопе. Для работы с бесконтактными (RFID/NFC) картами я использую популярный, недорогой и распространённый девайс ACS ACR122U, он подключается по USB, имеет драйвера для всех операционных систем и позволяет выполнять многие NFC-операции. Его легко купить как в России, так и заказать из Китая.
❈ ❈ ❈
Для использования смарт-карт с контактной площадкой (контактных смарт-карт) я купил другой терминал этого же производителя — ACS ACR38U-I1, это тоже очень популярный и недорогой девайс и тоже подключается через USB.
Драйверы для этих устройств можно скачать с официального сайта производителя:
- для ACS ACR122U — http://www.acs.com.hk/en/driver/3/acr122u-usb-nfc-reader/
- для ACS ACR38U-I1 — http://www.acs.com.hk/en/products/199/acr38u-i1-smart-card-reader/
Впрочем, драйвера для обоих устройств одинаковые, достаточно скачать и установить любой из пакетов. В линуксе нет необходимости их ставить, так как всё уже должно работать.
Вам может пригодиться документация к API этих терминалов (версии на 1 июня 2019 г.):
- для ACS ACR122U — https://www.acs.com.hk/en/download-manual/419/API-ACR122U-2.04.pdf
- для ACS ACR38U-I1 — https://www.acs.com.hk/download-manual/105/REF-ACR38x-CCID-6.05.pdf
Дальше по тексту и в коде примеров я буду называть считыватели (ридеры) смарт-карт или NFC-карт терминалами.
Ещё нам понадобится для экспериментов несколько разных смарт-карт. Например:
- банковские чиповые карты, включая карты с NFC (MasterCard PayPass, Visa PayWave);
- стандартная белая пустая бесконтактная карта из типичного комплекта ACR122U на aliexpress («фабричная NFC-карта», Mifare Classic 1K);
- стандартная белая пустая контактная карта (например, SLE5542 или SLE5528);
- Универсальная Электронная Карта (УЭК, хоть для чего-то пригодилась!);
- московская транспортная карта Тройка;
- новосибирская Единая Транспортная Карта;
- или транспортная карта вашего города;
- российский биометрический загранпаспорт (в нём есть NFC-чип);
- USB-токены (например, yubikey);
- NFC-метки;
- CAM-карта спутникового телевидения (НТВ-плюс, Триколор);
- SIM-карта любого оператора сотовой связи (чтобы её использовать в стандартном терминале, вам понадобится вставить её обратно в прорезь исходной карты формата ID-1 и закрепить скотчем);
- карта аутентификации для евро-тахографа.
❈ ❈ ❈
Базовая операционная система для экспериментов — Mac OS X или Debian/Ubuntu, но подойдёт любой другой дистрибутив linux. Язык программирования — Java 11. Все примеры кроссплатформенные и должны без модификаций собираться и запускаться на linux (debian/ubuntu) и Mac OS X.
Для macos вам достаточно поставить Java SE 11 Platform с сайта oracle: https://www.oracle.com/technetwork/java/javase/downloads/index.html.
Для линукса я рекомендую использовать openjdk11 из вашего репозитория, Oracle Java Platform с сайта oracle.com требует дополнительных манипуляций именно для работы смарт-карт, о которых я не хочу рассказывать. Для Debian/Ubuntu вам нужно поставить вот такие пакеты:
sudo apt install pcscd openjdk-11-jdk-headless make
Также не забудьте запустить демон pcscd:
sudo service pcscd start
Традиционный дисклеймер: в работе я активно использую терминал и shell. Все примеры запуска команд приводятся для shell-сессии вместе с результатом запуска.
Весь код лежит в моём репозитории на гитхабе. Фрагменты кода в тексте статьи предназначены только для иллюстрации и не обязательно дословно соответствуют коду соответствующего примера.
Я рекомендую такой процесс работы с текстом:
- скачайте репозиторий с гихаба
git clone https://github.com/sigsergv/pcsc-tutorial-java.git
- или архив https://github.com/sigsergv/pcsc-tutorial-java/archive/master.zip
- читайте текст
- изучайте примеры кода
- компилируйте и запускайте программы из соответствующего каталога
- модифицируйте программы как угодно.
Даже если у вас нет терминала для экспериментов, всё равно читайте разделы с примерами, там я тоже рассказываю о теоретических аспектах.
Весь код выпущен под лицензией New BSD License, вы можете его использовать где угодно при условии сохранения ссылки на автора или репозиторий на гитхабе. Код написан исключительно для образовательных и демонстрационных целей и не соответствует критериям промышленного качества.
Репозиторий разбит на каталоги с именами example-01
, example-02
и так далее. По умолчанию в каждом каталоге программа собирается и запускается командой make
. Все примеры кода в тексте сокращённые и даются без стандартной обёртки класса, списка импорта модулей и т.п. В начале каждого примера я даю ссылку на полный файл с гитхаба.
Отладка в Macos¶
Отладка в Macos Yosemite¶
В Macos Yosemite появился штатный инструмент для мониторинга активности терминалов смарт-/nfc-карт. Включается он вот такой командой перед подключением терминала к 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
Очень удобно открыть лог в отдельном окне shell-сессии и оставить там работать.
Отладка в Macos Sierra¶
С версии Macos Sierra (и вплоть до последней на момент написания этого текста версии Macos Ventura) логирование устроено по-другому. Сначала отключаем терминал от USB-порта, включаем логирование:
% sudo defaults write /Library/Preferences/com.apple.security.smartcard Logging -bool true
И снова подключаем. Теперь в консоли смотрим логи:
% log stream --predicate 'process == "com.apple.ifdreader"'
Отладка в 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 и является портом (с практически идентичным API) с Windows.
PC/SC является спецификацией, предложенной PC/SC Workgroup, именно работе с ней (и только с ней!) посвящена эта статья. Другие низкоуровневые команды, протоколы и спецификации не рассматриваются. Работа с NFC также происходит исключительно через PC/SC.
Мы будем пользоваться входящим в стандартную поставку Java пакетом javax.smartcardio, который инкапсулирует операции PC/SC в виде набора классов и фабрик. javax.smartcardio использует единственный PCSC-контекст для всех потоков программы, поэтому вам нужно все операции выполнять последовательно, имейте это в виду, когда будете писать сложную программу.
example-01: инициализация библиотеки¶
Для этого примера можно использовать оба типа терминалов: для бесконтактных и контактных чиповых карт.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-01/Example.java
Цели этого примера:
- дать общее представление, как выглядит работа с терминалом смарт-карты в программном коде;
- на очень простом и компактном примере получить осязаемый результат;
- о терминах и теории поговорим дальше, сначала — первая работающая программа.
Что конкретно нам нужно сделать:
- подключиться к системной библиотеке PC/SC через предоставляемую Java библиотеку;
- получить список доступных терминалов;
- напечатать его на экране.
Начнём с импорта всех классов модуля (полная документация по ссылке https://docs.oracle.com/en/java/javase/11/docs/api/java.smartcardio/javax/smartcardio/package-summary.html):
import javax.smartcardio.*;
Получим список всех терминалов:
var factory = TerminalFactory.getDefault();
var terminals = factory.terminals().list();
TerminalFactory
и CardTerminal
— это классы из модуля javax.smartcardio
.
И теперь мы можем распечатать пронумерованный список терминалов:
System.out.println("Found terminals:");
int i = 0;
for (var t : terminals) {
i++;
System.out.printf(" Terminal #%d: %s %n", i, t.getName());
}
Перед запуском программы подключите имеющиеся у вас терминалы к компьютеру (вставлять карту необязательно). В моём случае результат выполнения в macos выглядит так:
Found terminals:
Terminal #1: PC/SC terminal Yubico Yubikey 4 OTP+U2F+CCID
Terminal #2: PC/SC terminal ACS ACR122U
Здесь Terminal #1
— это включенный в USB-порт ключ Yubikey 4, а Terminal #2
— это USB-ридер бесконтактных смарт-карт и NFC.
example-02: подключение к терминалу¶
Для этого примера можно использовать оба типа терминалов: для бесконтактных и контактных чиповых карт.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-02/Example.java
Цели этого примера:
- подключиться к конкретному терминалу;
- если в нём нет карты, ждать, пока она там не появится;
- прочитать что-нибудь с карты.
Для обработки ошибок определим новое исключение:
public static class TerminalNotFoundException extends Exception {}
Начало как в прошлом примере, то есть, запрашиваем фабрику, а из неё список терминалов. Для простоты возьмём просто первый терминал из списка, а если список пустой, то выбросим собственное исключение TerminalNotFoundException
.
var factory = TerminalFactory.getDefault();
var terminals = factory.terminals().list();
if (terminals.size() == 0) {
throw new TerminalNotFoundException();
}
// get first terminal
var terminal = terminals.get(0);
Теперь мы хотим дождаться появления карты в терминале и потом уже подключиться к ней. Если попытаемся подключиться сразу, то при отсутствии карты поймаем исключение javax.smartcardio.CardException
.
// wait for card, indefinitely until card appears
terminal.waitForCardPresent(0);
// establish a connection to the card using any available protocol ("*")
var card = terminal.connect("*");
Для подключения к карте в методе connect()
мы должны передать название предпочитаемого транспортного протокола. Их принято обозначать строками T=0
и T=1
. T=0
исторически первый, он байто-ориентированный. T=1
— более поздний, он блок-ориентированный. Оба протокола полу-дуплексные асинхронные. Протокол определяет формат отправки и получения данных. На данный момент нам всё равно, каким протоколом подключаться, поэтому указываем звёздочку (*
) и терминал сам выберет подходящий. Более того, многие терминалы сами обеспечивают прозрачное преобразование запросов из одного протокола в другой и при инициализации можно всегда указывать звёздочку.
Мы пока ничего ничего не знаем о данных или функциях карты, однако у них у всех есть общее свойство — ATR (Answer-To-Reset) — это короткий (не более 33) массив байтов, который карта обязана передать терминалу при подключении. Если карта этого не делает за определённое время, считается, что она функционирует некорректно. В ATR содержится базовая информация о карте и технических параметрах соединения, но его формат довольно сложный и зависит от производителя чипа, установленных на карте «приложений» и так далее. ATR сохраняется в терминале всё время, пока карта подключена.
Структура ATR определена в разделе 8.2 стандарта ISO/IEC 7816-3, а я детально расскажу об этом позднее, пока ограничимся только его байтовым представлением.
System.out.printf(" Card protocol: %s%n", card.getProtocol());
System.out.printf(" Card ATR: %s%n", hexify(card.getATR().getData()));
Для печати байтов в hex-формате я использую локально заданный в этом классе метод hexify()
, вы можете его код посмотреть в исходниках.
И, наконец, отключаемся от карты:
card.disconnect(true);
После этой команды объект card
больше нельзя использовать для операций с картой. Аргумент метода определяет, должна ли для карты производиться операция RESET после отключения. RESET — это начальная стадия коммуникации с картой после подачи питания на чип. После выполнения команды карта переходит в то же состояние, в которое она попадает после подключения.
Вот результат выполнения программы на чистой «фабричной» NFC-карте и терминале ACR122U:
Using terminal PC/SC terminal ACS ACR122U
Card protocol: T=0
Card ATR: 3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 01 00 00 00 00 6A
А вот — на USB-ключе yubikey:
Using terminal PC/SC terminal Yubico Yubikey 4 OTP+U2F+CCID
Card protocol: T=1
Card ATR: 3B F8 13 00 00 81 31 FE 15 59 75 62 69 6B 65 79 34 D4
Важные моменты из этого примера
- Нужно явным образом дожидаться готовности терминала и появления в нём карты.
- После подключения к карте все операции с ней выполняются через объект класса
javax.smartcardio.Card
.- После окончания работы с картой нужно обязательно отключиться методом
disconnect()
, если этого не сделать, терминал может «зависнуть» и потребуется его физическое переподключение.- Карта при подключении к терминалу обязана сразу же передать свой ATR.
- ATR не является идентификатором карты и не содержит идентификатор карты. Все карты одного типа могут иметь одинаковый ATR.
- В интернете есть вебсервис, которым можно парсить ATR: https://smartcard-atr.apdu.fr/
- Поэкспериментируйте с ATR разных карт или NFC-меток.
- Вы можете вместо карты поднести к терминалу, например, смартфон с включённым NFC, терминал его распознает и программа покажет его ATR.
- У бесконтактной карты (NFC) нет ATR, так как там иная процедура инициализации, однако терминал для совместимости генерирует ATR сам и отдаёт его программе.
Теория: архитектура PC/SC¶
Термины и сокращения¶
Сначала несколько терминов и сокращений, которые активно используются в стандартах (и дальше у меня в тексте).
- 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-сканер.
❈ ❈ ❈
Тип
byte
в Java является знаковым, как и все остальные числовые типы, то есть диапазон его значений:-128..127
. Однако везде в спецификациях и стандартах нашей предметной области предполагается, что байт — беззнаковый, поэтому вы не сможете присвоить переменной типа byte значение, скажем,0xFA
без явного преобразования типа. В дальнейшем я на этом не буду заострять внимание и предполагаю, что вы в курсе такого поведения.
В стандартах и спецификациях приняты определённые соглашения относительно записи байтов и байтовых массивов. Размер байта — 8 бит. Нотация для байта следующая: пара hex-символов в верхнем регистре, например 02
или F1
. В некоторых документах встречается такая запись: 02h
или F1h
.
В некоторых сетевых стандартах принято называть байт октетом (octet), чтобы подчеркнуть его размер — восемь бит. В нашей области термин октет не используется.
Байтовые массивы описываются в виде списка байтов, для читаемости разделённых пробелами: 00 0A 00 12 AC
. Это именно последовательность байтов в том виде, в каком она хранятся в памяти или передаются по каналу связи.
Байт F9
состоит из двух полубайтов (по-английски — nibble): F
и 9
. Запись вида 0X
означает: «все байты, в которых первый/старший полубайт равен 0
».
Биты, из которых состоит байт, принято нумеровать справа налево в бинарной записи. Например, байт C5
в битовом представлении выглядит так: 1 1 0 0 0 1 0 1
. А биты нумеруются так:
C5
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ b8 │ b7 │ b6 │ b5 │ b4 │ b3 │ b2 │ b1 │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ 1 │ 1 │ 0 │ 0 │ 0 │ 1 │ 0 │ 1 │
└────┴────┴────┴────┴────┴────┴────┴────┘
При этом бит b8
принятно называть старшим битом (higher bit, most significant), а b1
— младшим (lower, least significant). Также выражение типа «три старших бита» означает «биты b8, b7, b6».
Для обозначения чисел в шестнадцатеричном виде используется C-нотация, например, 0x12A9
или 0x35
. Иногда ещё указывается, в каком виде кодируется число — little endian / big endian. Однако в нашей предметной области (как и вообще в сетях) принят по умолчанию вариант big endian. То есть в памяти первым идёт старший байт.
Иерархичная модель¶
Смарт-карта фактически является миниатюрным компьютером с микропроцессором, памятью и операционной системой. Внешние системы не имеют прямого доступа к его данным и должны пользоваться предоставленным интерфейсом (контактная площадка или радиоинтерфейс). У смарт-карт нет собственного питания и они получают его от терминала через контакты (ICC) или индукционную катушку (PICC).
Цель спецификации PC/SC Workgroup — предоставить единый унифицированный интерфейс для работы с любыми IFD/PCD. Спецификации PC/SC доступны для свободного скачивания на сайте организации: https://pcscworkgroup.com/specifications/
Архитектура PC/SC иерархичная.
На самом нижнем уровне находятся физические устройства для считывания (терминалы для чтения карт) — IFD/PCD и собственно карты — ICC/PICC. Между картами и терминалами используется электрический сигнальный протокол, он предельно низкоуровневый и оперирует электрическими характеристиками (частота, напряжение и так далее). У каждого IFD может быть свой способ подключения к компьютеру (USB, COM, Firewire и т.д.), свой собственный протокол для работы через порт подключения.
Уровнем выше располагаются IFD Handler, по сути это что-то вроде драйверов, которые преобразуют специфичный интерфейс каждого IFD в унифицированный, который используется уровнем выше.
На следующем уровне — ICC Resource Manager, менеджер ресурсов — это ключевой компонент всей инфраструктуры, он должен быть единственным в ОС и, скорее всего, предоставляется операционной системой. ICC Resource Manager управляет доступом до всех ICC и FD, отслеживает появление в системе новых терминалов, подключение и отключение ICC/PICC. Также задачей менеджера является разделение доступа до IFD и транзакции. Именно через него должны проходить все коммуникации между прикладным/системным ПО и ридерами. Никакое другое ПО не должно иметь самостоятельного доступа до непосредственно IFD Handler. Однако, терминал вполне может быть доступен как USB-устройство, например, то есть какой-то другой драйвер может работать параллельно с IFD handler.
В линуксе роль ICC Resource Manager выполняет демон pcscd из пакета pcsc-lite. В макоси и windows менеджер является стандартной частью операционной системы.
Выше ICC Resource Manager по иерархии находятся все остальные компоненты (сервис-провайдеры) и программы операционной системы. Они могут работать как напрямую через менеджер, так и через друг-друга. На картинке выше нарисовано несколько таких сервис-провайдеров. Более подробно о них написано во вводной части к спецификации PC/SC. Один из самых распространённых сервис-провайдеров — крипто-провайдер, который предоставляет для прикладных приложений унифицированный интерфейс взаимодействия с криптографическими приложениями на ICC/PICC.
На последнем уровне находятся прикладные приложения. Они могут работать как с промежуточными сервис-провайдерами, так и напрямую с ICC Resource Manager. Естественно, приложение должно уметь работать с интерфейсом сервис-провайдера или с низкоуровневым интерфейсом менеджера.
Я лишь коротко рассказал об основных компонентах в архитектуре PC/SC, детально они описаны в официальном стандарте. Дальше в тексте я буду иногда использовать только что описанные термины и сокращения, например, вместо «терминал» я буду писать «IFD», вместо «смарт-карта» — «ICC», а вместо «NFC-карта» — «PICC».
Схема работы IFD и ICC¶
Рассмотрим сначала диаграмму состояний IFD и ICC (напомню, что ICC — это чиповая карта с контактной площадкой, в этом разделе рассматриваются только они).
Концептуально действительно всё просто: при подключении карты (подаче питания) отправляется сигнал RESET. Это делается через выделенную контактную площадку RST. Чип на карте понимает, что пришло питание и пошёл процесс инициализации, значит нужно отправить назад специальный байтовый массив ATR (расшифровывается как Answer-to-Reset). Карта должна это сделать очень быстро, иначе IFD посчитает её неисправной и прекратит взаимодействие.
О структуре ATR я рассказываю подробно дальше в этой статье. Однако сейчас отмечу два важных момента:
- ATR генерируется микропроцессором контактной чиповой карты.
- В ATR содержится необходимая для дальнейшей работы информация: протоколы, код производителя, параметры протоколов и так далее. ATR не является идентификатором карты! Главная его цель — корректная инициализация оборудования для дальнейшей работы.
Когда IFD получает ATR, он его парсит, извлекает нужные для корректного соединения параметры и переключается в готовый для дальнейшего взаимодействия режим. В этом режиме коммуникации между IFD и ICC происходят путём отправки и получения TPDU — Transport Protocol Data Unit. А между ICC и прикладными программами общение происходит посредством APDU — Application Protocol Data Unit. Отправка APDU и обработка ответов — это главное, чем занимается программа, работающая с PC/SC. ICC преобразует APDU в TPDU, отправляет TPDU в карту, получает ответ также в виде TPDU и преобразует его в APDU.
Схема работы PCD и 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 и логически выглядит примерно так же, как на иллюстрации из предыдущего раздела.
Обмен данными: термины и сокращения¶
Физические аспекты протокола для общения с чиповой картой (с контактной площадкой) определены в стандарте ISO/IEC 7816-3 (ГОСТ Р ИСО:МЭК 7816-3, это нас не интересует, поскольку за это отвечают компоненты PC/SC.
Собственно протокол для приложений определён в стандарте ISO/IEC 7816-4 (ГОСТ Р ИСО:МЭК 7816-4). В нём описывается формат команд и ответов APDU, структуры данных и приложений.
Сначала несколько терминов и сокращений (на базе определений из ГОСТ Р ИСО:МЭК 7816-4:2013, исправленных для нормального восприятия). Весь список я тут приводить не буду, только самое главные, мы их дальше будем активно использовать.
- Application
- Структуры, элементы данных и программные модули (на ICC/PICC), необходимые для выполнения определенных функций. Примеры приложений: банковское (Visa/MasterCard), телефонное (SIM для GSM-сети).
- APDU
- application protocol data unit, блок данных прикладного протокола: основной тип данных, который используется для связи ICC/PICC и IFD/PCD. APDU представляет собой байтовый массив с определённой логической структурой.
Структура пары команда-ответ¶
«Драйверы» из PS/SC берут на себя всю низкоуровневую работу с протоколами, клиентам остаётся только сравнительно высокоуровневая часть: отправка и получение байтов стандартизированным образом.
Взаимодействие с NFC-картами происходит через эмуляцию протокола T=1, такой протокол также называется
T=CL
. Однако для приложения работа с NFC-картой через PC/SC ничем не отличается от работы с картой с контактной площадкой.
Пара команда-ответ состоит из двух идущих подряд сообщений: APDU команды (от прикладной программы через терминал к карте) и APDU ответа (от карты через терминал к прикладной программе). Каждый APDU представляет собой последовательность байтов со следующей структурой (Таблица 1 из раздела 5.1 стандарта ISO/IEC 7816-4):
Это достаточно сложное описание, к этой таблице в ISO/IEC 7816-4 прилагается подробное объяснение, рекомендую посмотреть его, если хотите полностью разобраться.
К счастью, всего существует четыре структурно отличающихся варианта APDU команды, их принято называть Case 1, Case 2, Case 3 и Case 4. Вот их структура:
Case 1:
┌─────┬─────┬────┬────┐
│ CLA │ INS │ P1 │ P2 │
└─────┴─────┴────┴────┘
Case 2:
┌─────┬─────┬────┬────┬────┐
│ CLA │ INS │ P1 │ P2 │ Le │
└─────┴─────┴────┴────┴────┘
Case 3:
┌─────┬─────┬────┬────┬────┬─┬─┬─┬─┬─┬─┐
│ CLA │ INS │ P1 │ P2 │ Lc │ DATA │ длина DATA равна значению Lc
└─────┴─────┴────┴────┴────┴─┴─┴─┴─┴─┴─┘
Case 4:
┌─────┬─────┬────┬────┬────┬─┬─┬─┬─┬─┬─┬────┐
│ CLA │ INS │ P1 │ P2 │ Lc │ DATA │ Le │ длина DATA равна значению Lc
└─────┴─────┴────┴────┴────┴─┴─┴─┴─┴─┴─┴────┘
APDU команды (также называется C-APDU) начинается с байта CLA
— это класс команды, он задаёт параметры коммуникации, его структура детально описана в разделе 5.1.1 стандарта ISO/IEC 7816-4. Например, самый старший бит определяет принадлежность команды: если он установлен в 1, то это проприетарная команда, не описанная в стандарте ISO/IEC 7816-4. Вот несколько примеров CLA
:
CLA │ Описание
═════╪══════════════════════════════════════════════════════════════════════════════
00 │ структура команды по ISO/IEC 7816-4, без защиты сообщений, логический канал 0
─────┼──────────────────────────────────────────────────────────────────────────────
8X │ электронный кошелёк/банковские карты
─────┼──────────────────────────────────────────────────────────────────────────────
A0 │ SIM-карты для сотовой связи
─────┼──────────────────────────────────────────────────────────────────────────────
FF │ используется для бесконтактных карт в рамках спецификации PC/SC
Можно считать, что CLA задаёт «пространство имён» (namespace) для команд.
❈ ❈ ❈
Командный байт INS
(сокращение от instruction) определяет собственно команду (функцию, инструкцию — эти термины взаимозаменяемы и я их все буду использовать). В отрасли принято использовать одинаковые коды инструкций для близких по смыслу операций. Стандартные межотраслевые команды приведены в ISO/IEC 7816-4, а в конкретных отраслях они используются в собственных проприетарных классах CLA.
❈ ❈ ❈
Следующие два байта P1 и P2 — это параметры команды, их семантика зависит от конкретных CLA
и INS
.
CLA
, INS
, P1
и P2
— это четыре обязательных поля любой С-APDU, они образуют заголовок команды и присутствуют во всех вариантах C-APDU.
❈ ❈ ❈
Поле Le
(сокращение от Length, expected) задаёт, какой длины ответ мы хотим получить. Значение Le
равное нулю означает «хотим получить все данные». Длина этого поля может быть 1, 2 или 3 байта, в стандарте описывается, как оно формируется. Если Le
отсутствует (Case 1, Case 3), то ответ на команду (без учёта статусных байтов, о них ниже) ожидается нулевой длины (однако это правило часто игнорируется).
❈ ❈ ❈
В поле Lc (сокращение от Length, command) задаётся размер дополнительного блока данных команды в байтах, если данных нет, то это поле должно отсутствовать.
Если Lc
присутствует и содержит значение больше нуля (это значение принято обозначать как Nc
), то далее идёт блок дополнительных данных команды ровно из Nc
байтов.
❈ ❈ ❈
После отправки C-APDU в терминал, он возвращает ответ в виде R-APDU. Длина ответа должна быть как минимум два байта. В двух последних содержится статус выполнения команды (в стандарте они называются status word bytes), а в остальных — полезные данных. Если команда не возвращает полезных данных, R-APDU имеет следующий вид:
┌─────┬─────┐
│ SW1 │ SW2 │
└─────┴─────┘
Если возвращаются данные, то такой:
┌┬─┬─┬─┬────┬─┬─┬┬─────┬─────┐
│ RESPONSE DATA │ SW1 │ SW2 │
└┴─┴─┴─┴────┴─┴─┴┴─────┴─────┘
Допустимые значения SW1/SW2 также определены в ISO/IEC 7816-4. Принято записывать значение статуса в виде hex-значения 0xXXYY
, где XX — это SW1, а YY — SW2. 0xXXYY
также иногда называют статусным словом (SW как раз означает Status Word). Например, статус успешного завершения команды — 90 00
, а целиком R-APDU может выглядеть так: 01 02 03 00 90 00
. Запись 61XX
означает, что SW1 = 61
, а SW2
может принимать любое значение.
INS
,CLA
,P1
и т.д. — это стандартизированные названия соответствующих байтов/полей в массиве байтов APDU, на них можно ссылаться при описании различных APDU. Они фигурируют во всех стандартах, так или иначе пересекающихся с ISO/IEC 7816.
❈ ❈ ❈
Любое взаимодействие с терминалом (и соответственно с картой) происходит через отправку и получение APDU. Также у каждого терминала бывают внутренние команды, которые не передаются на карту. Например, для управления светодиодами на корпусе используются псевдо-команды или псевдо-APDU, они используют зарезервированные CLA
и INS
для собственных операций.
❈ ❈ ❈
Рассмотрим команду Get Data для бесконтактных карт (описывается в разделе 3.2.2.1.3 спецификации PC/SC part 3). Код инструкции для неё (INS=CA
) задан в стандарте ISO/IEC 7816-4, однако для бесконтактных карт семантика параметров и результата отличаются от изначальных. В зависимости от значения P1 инструкция возвращает либо UID, либо байты предыстории (historical bytes) из ATS (ATS — Answer-to-Select — некий аналог ATR для PICC, участвующий в процессе инициализации бесконтактной карты в PCD).
Вот байты команды:
FF CA 00 00 00
Поле CLA
здесь — FF
, это значение является зарезервированным и в стандарте ISO/IEC 7816-4 явным образом отмечено как невалидное (раздел 5.1.1), то есть команды с таким классом нельзя использовать для работы с картами. Поэтому в PC/SC этот класс используется для команд, предназначенных для терминала, это описано в разделе 3.2 спецификации PC/SC part 3. Команды в этом классе обрабатываются самим терминалом и преобразуются в низкоуровневые команды карты.
Поле INS
— CA
, это код инструкции Get Data.
Параметры команды:
P1=00
,P2=00
— для получения UIDP1=01
,P2=00
— для получения байтов предыстории из ATS (пока не задумывайтесь, что такое байты предыстории, просто некие данные)
Поле Lc
отсутствует и, значит, отсутствует следующий за ним блок данных команды. То есть Nc=0
.
В поле Le стоит значение 00
, то есть мы ожидаем все данные сразу.
В следующем примере я покажу, как отправлять команды и получать ответы.
example-03: Получение UID бесконтактной карты¶
Для этого примера можно использовать только терминал для бесконтактных чиповых карт. Я использую бесконтактные карты потому, что для них гарантированно можно выполнить фиксированную команду с успешным результатом, а для контактных карт такое в общем случае неверно.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-03/Example.java
Цели этого примера:
- показать, как выглядит в коде отправка команд и получение ответов;
- показать, как можно работать с массивами байтов в Java.
Начало как в прошлых примерах, поэтому я его не привожу: получаем список терминалов, берём первый, ждём подсоединения карты, подключаемся к карте:
// establish a connection to the card using autoselected protocol
var card = terminal.connect("*");
После подключения мы должны получить логический канал (logical channel, подробно описаны в разделе 5.1.1.2 ISO/IEC 7816-4); логические каналы используются, когда нужно одновременно работать с несколькими приложениями на карте. На данном этапе нам достаточно того, что так называемый основной канал (basic channel) обязан существовать всегда (далеко не все карты поддерживают несколько каналов):
// obtain logical channel
var channel = card.getBasicChannel();
APDU инструкции (напомню, это расшифровывается как application protocol data unit) для получения UID бесконтактной карты состоит из следующих байтов: FF CA 00 00 00
. Так как в Java числовые типы знаковые, мы не можем напрямую использовать hex-литералы для инициализации массива байтов (byte[]
), воспользуемся методом toByteArray()
, определённым в нашем же классе, который преобразует список целочисленных значений из диапазона [0,..,255]
в список байтов, который уже можно передавать в другие методы. А для печати байтового массива в hex-формате используем тоже наш метод hexify()
:
// execute command
int[] command = {0xFF, 0xCA, 0x00, 0x00, 0x00};
ResponseAPDU answer = channel.transmit(new CommandAPDU(toByteArray(command)));
System.out.printf("Card UID: %s%n", hexify(answer.getBytes()));
Для отправки команды сначала создаётся массив hex-значений, затем он конвертируется в байтовый массив, который используется в конструкторе класса javax.smartcardio.CommandAPDU
и дальше этот объект передаётся в метод transmit()
ранее полученного канала. Метод возвращает результат в виде объекта класса javax.smartcardio.ResponseAPDU
.
В дальнейших примерах я не буду пользоваться этим методом задания массива байтов, поскольку в таком виде их трудно как читать, так и писать. Вместо этого я напишу конвертер строкового представления байтового массива в собственно массив, то есть строки вида
"FF CA 00"
в массив{(byte)0xFF, 0xCA, 0x00}
.
Дальше мы проверяем результат, если значение SW не 0x9000, то команда не выполнилась и мы выбрасываем наше локальное исключение InstructionFailedException
:
if (answer.getSW() != 0x9000) {
throw new InstructionFailedException();
}
Данные ответа включают в себя SW в конце блока, поэтому мы вызываем метод getData()
, который возвращает ответ без последних двух байтов:
var uidBytes = answer.getData();
System.out.printf("Card UID: %s%n", hexify(uidBytes));
Вот результат выполнения программы:
Using terminal PC/SC terminal ACS ACR122U
Card UID: 9C 9C AC 99
Если мы попытаемся запустить его для терминала карт с контактной площадкой, то получим ошибку:
Using terminal PC/SC terminal ACS ACR38U-CCID
CardException: javax.smartcardio.CardException: sun.security.smartcardio.PCSCException: SCARD_E_NOT_TRANSACTED
Это происходит потому, что терминал/карта не поддерживают такие операции.
UID бесконтактной карты определяется в разделе 6.4.4 стандарта ISO/IEC 14443-3 и может быть длиной 4, 7 или 10 байтов. UID в общем случае НЕ является постоянным уникальным идентификатором карты — стандарт допускает его динамическую случайную генерацию (картой). Основная цель UID — идентификация карты в цикле антиколлизии. Считается, что вероятность совпадения UID нескольких карт, поднесённых к одному терминалу, исчезающе мала.
Некоторые производители карт гарантируют, что все выпущенные ими чипы/карты размером 7 байтов уникальны. Однако не стоит на это полагаться.
❈ ❈ ❈
Теория: данные на смарт-карте¶
Типы карт по способу управления данными¶
Смарт-карты (и контактные, и бесконтактные) делятся на две группы:
- микропроцессорные карты — содержат микропроцессор и встроенное ПО для управления данными в памяти карты; они используются, например, в банковских картах, электронных паспортах, SIM-картах;
- карты памяти — содержат только данные и, возможно, средства разграничения доступа к ним; используются, например, как электронные проездные или карты оплаты в телефонах. Такие карты существенно дешевле микропроцессорных.
Микропроцессорная карта является мини-компьютером с процессором и памятью, в этом «компьютере» могут храниться данные и запускаться приложения. Типичный пример приложения — криптографическое, которое использует хранимый в постоянной памяти смарт-карты секретный ключ для шифрования/дешифрования данных извне карты, причём снаружи карты доступа к приватному ключу нет.
Логически любая микропроцессорная карта состоит из одних и тех же компонентов, вот они на схеме вместе со связями.
- 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) устроена существенно проще, внутри неё нет полноценного микропроцессора, а только простой недорогой чип, умеющий выполнять фиксированные несложные операции. Устроена она примерно так:
- I/O subsystem
- Подсистема ввода/вывода, через неё проходят все данные из карты и в карту.
- ROM
- Read only memory — постоянная память, записанные на ней данные нельзя изменить. На картах памяти в ROM хранится, например, уникальный идентификатор карты, который присваивается при её изготовлении.
- EEPROM
- Electronically erasable programmable read only memory — перезаписываемая память, содержимое которой не исчезает после отключения питания. Размер этой памяти для карт данного типа обычно очень небольшой, измеряется буквально килобайтами.
- Логика адресации и разграничения доступа
- Это простой чип, обычно проприетарный с закрытой архитектурой, его роль состоит в обработке сигналов от подсистемы ввода-вывода и запись/чтение блоков памяти в EEPROM. Также этот чип отвечает за разграничение доступа к определённым блокам памяти.
❈ ❈ ❈
Самыми распространёнными бесконтактными картами памяти являются Mifare Classic. Они выпускаются как в виде традиционных пластиковых карт, так и виде брелков, наклеек, колец и т.п. Самый известный пример их использования — разнообразные транспортные карты: московская «Тройка», питерский «Подорожник», транспортная карта компании «Золотая корона» и так далее.
Работа с картами памяти Mifare Classic 1K¶
У Mifare собственный сигнальный протокол (между картой и терминалом) и своя собственная схема работы с данными. Эти карты не следуют стандарту ISO/IEC 7816, поэтому не понимают описанные там стандартные команды. Однако в PC/SC определены инструкции в классе FF
, которые многие терминалы понимают и корректно транслируют в низкоуровневые команды для конкретного типа карты, в нашем случае — это Mifare. Полную спецификацию протокола карты можно прочитать в официальном документе производителя этих карт NXP: MIFARE Classic EV1 1K datasheet.
На карте Mifare Classic 1K один килобайт (1024 байта) в перезаписываемой памяти EEPROM, эта память разделена на 16 секторов, доступ к каждому сектору контролируется двумя разными ключами, которые обозначаются как Ключ A и Ключ B. Каждый сектор разделён на 4 блока по 16 байтов.
«Адреса» секторов начинаются с нуля: первый имеет адрес 0x0
, второй — 0x1
и так далее до шестнадцатого сектора с адресом 0xf
.
У блоков адресация сквозная через сектора и начинаются тоже с нуля, например, сектор 0x0
состоит из блоков с адресами {0x0, 0x1, 0x2, 0x3}
, сектор 0x1
— из блоков {0x4, 0x5, 0x6, 0x7}
, а сектор 0xF
— из блоков {0x3C, 0x3D, 0x3E, 0x3F}
.
Первый блок памяти карты (с адресом 0x0
) зарезервирован под данные производителя и содержит, в частности, UID. Теоретически он защищён от перезаписи, однако существуют «хакерские» карты, где данные в этом блоке можно изменить.
❈ ❈ ❈
Внешние клиенты не имеют прямого доступа к этим блокам и не могут записывать туда произвольную информацию. Также нельзя использовать все 1024 байта для записи данных, часть этого места зарезервирована для хранения ключей и условий доступа. Можно читать или писать только блок целиком (то есть 16 байт), если это позволяют условия доступа. Писать/читать отдельные байты или произвольные участки памяти нельзя.
Последний блок каждого сектора называется трейлером сектора (sector trailer) и содержит (именно в таком порядке с нулевого байта): Ключ A (6 байтов, обязательно), условия доступа (access conditions, access bits, access control bits, 3 байта), пользовательский байт, Ключ B (6 байтов). Вместо Ключа B, если он не нужен, можно хранить любые другие данные.
Перед любой операцией с памятью карты требуется обязательная аутентификация, она выполняется отдельно для каждого из блоков, к данным которых нужен доступ. Для аутентификации используется упомянутый ранее Ключ A (или Ключ B). Для всех операций внешние клиенты используют специальный инструкции, которые затем транслируются терминалом в протокол карты. Операция чтения/записи выполняется за три инструкции:
- записать в память терминала 6 байтов ключа;
- выполнить команду аутентификации для нужного блока;
- выполнить нужную операцию с этим блоком.
PCD (Proxymity Coupling Device) транслирует C-APDU в сигнальный протокол карты, а потом так же транслирует ответ назад в R-APDU.
Допустимые операции с памятью сектора определяются условиями доступа из трейлера сектора. Для каждого блока можно установить условия доступа (на операции Read, Write, Increment, Decrement, Restore) и какой ключ использовать для аутентификации (Ключ A или Ключ B). Отдельно задаются условия доступа к блоку с трейлером (read, write).
Каждый блок в зависимости от параметров в условиях доступа может быть либо бинарным блоком (read/write block, то есть набором байтов, который можно прочитать и записать), либо числовым блоком (value block, то есть в блоке хранится целое число размером 4 байта). К числовому значению в блоке помимо операций Read и Write можно также применять операции Increment, Decrement, Transfer и Restore. Числовое значение — это четырёхбайтовое знаковое целое, в блоке может храниться только одно значение, внутри байтов блока оно повторяется три раза для контроля целостности.
Работа с числовыми блоками выходит за границы спецификации PC/SC (и этой статьи), каждый производитель терминалов реализует собственные команды для этого.
❈ ❈ ❈
Условия доступа для каждого блока представляют собой три бита. Они особым образом упакованы в трёх байтах из трейлера. В спецификации подробно описан способ хранения, а также приводятся таблицы, объясняющие, как именно нужно интерпретировать различные сочетания этих битов для всех типов блоков. Подробнее о битах доступа я расскажу в одном из примеров.
Значение Ключа A можно только записать, но нельзя прочитать, эти байты всегда возвращаются заполненными нулями. Значение Ключа B можно сделать читаемым определённым сочетанием битов доступа.
Теоретически такой подход к управлению данными позволяет добиться весьма впечатляющих результатов. Можно использовать разные наборы ключей A и B, чтобы сохранять в нужные блоки нужную информацию и безопасно её обновлять. Однако этот формат карты давно взломан и поэтому совершенно не безопасен: всю информацию (и ключи тоже) можно легко извлечь, а при желании можно сделать полную копию карты, используя специальные китайские карты-болванки.
Жизненный цикл карты Mifare Classic обычно такой:
- На заводе производится чип со стандартными ключами и условиями доступа.
- Партия чипов поставляется производителю носителей.
- Производитель вставляет чипы в различные оболочки: карту, брелок, наклейку итп. Возможно ещё и рисует что-нибудь на карте.
- Карта поставляется оптовой партией сервис-провайдеру, например, метрополитену.
- Провайдер персонифицирует карту, то есть заполняет её нужными значениями, регистрирует в своей базе идентификатор, выставляет новые условия доступа и меняет ключи.
- Карта попадает к конечному пользователю.
Когда я пишу «карта», я имею в виду чип, физически носитель этого чипа может быть наклейкой, брелком и т.д.
Помимо Mifare Classic существуют более защищённые микропроцессорные карты, но у них свой протокол, устроенный совершенно по-другому. Также существуют более защищённые карты Mifare Plus, у которых есть режим обратной совместимости с Mifare Classic, однако в этом режиме они точно так же уязвимы.
А теперь несколько практических примеров работы с картой Mifare Classic. После них я расскажу о микропроцессорных картах.
example-04: Чтение карты Mifare Classic¶
Для этого примера можно использовать только терминал для бесконтактных чиповых карт.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-04/Example.java
Цели этого примера:
- подготовка терминала для чтения данных (аутентификация) в соответствии со спецификацией PC/SC;
- чтение данных c картs Mifare Classic 1K;
- обработка ошибок;
- вынесение часто используемого кода в отдельные методы для переиспользования.
Вам понадобится какая-нибудь карта Mifare Classic 1K, для которой вы знаете хотя бы один ключ доступа. Идеально подойдёт чистая заводская неперсонифицированная карта. Также можете попробовать бесконтактный транспортный проездной («Тройка», «Подорожник», «Единая транспортная карта»), они часто сделаны на базе чипов Mifare Classic 1K. Даже если вы не знаете ключей доступа, то всё равно можете попробовать выполнить пример для имеющихся у вас карт.
Я пропускаю код для базовых шагов и предполагаю, что мы уже инициировали соединение с библиотекой, прочитали список терминалов, выбрали первый и подключились к поднесённой карте.
Чтобы получить доступ к данным на карте, мы должны провести аутентификацию с правильным ключом. Этот процесс проходит в два этапа: сначала отправляем терминалу ключ, чтобы он его запомнил в своей внутренней памяти, а затем отправляем команду аутентификации со ссылкой на запомненный ключ.
Для формирования байтового массива с APDU мы будем пользоваться нашим новым методом toByteArray()
, который будет принимать на вход текстовую строку с кодами байтов (типа AF 02 4E 12
), а выдавать байтовый массив. Его код вы можете посмотреть в полном исходном тексте примера.
Для новой карты (чипа) производитель устанавливает фиксированные ключи, некоторые из них приведены в таблице ниже:
Производитель │ Ключ A │ Ключ B
══════════════╪═════════════════════╪════════════════════
NXP │ FF FF FF FF FF FF │ FF FF FF FF FF FF
──────────────┼─────────────────────┼────────────────────
Infineon │ A0 A1 A2 A3 A4 A5 │ B0 B1 B2 B3 B4 B5
Первый этап: запись ключа в памяти терминала. Это делается всё тем же методом transmit()
для передачи APDU команды Load Keys (определена в спецификации PC/SC part 3, раздел 3.2.2.1.4):
byte[] command;
// 1. load key
// CLA INS P1 P2 Lc Data
command = toByteArray("FF 82 00 00 06 FF FF FF FF FF FF");
answer = channel.transmit(new CommandAPDU(command));
if (answer.getSW() != 0x9000) {
System.out.printf("Response failed: %s%n", hexify(answer.getBytes()));
throw new InstructionFailedException();
}
APDU команды выглядит так: FF 82 00 00 06 FF FF FF FF FF FF
CLA = FF
, напомню, что значениеFF
не разрешено в ISO 7816 и в этом классе передаются команды для терминалаINS = 82
, код инструкции Load KeysP1 = 00
, набор битов, задающий структуру ключа; в данном случае00
означает, что ключ сохраняется в волатильной памяти, то есть терминал его забудет после отключения питания, ACR122U другие режимы не поддерживает. Иные модели терминалов могут сохранять ключ в постоянной памяти.P2 = 00
, номер ключа, для ACR122U здесь может стоять либо00
, либо01
. По сути это условный номер ячейки памяти терминала, в которую записать ключ, это же значение мы передадим в следующей инструкции.Lc = 06
, длина блока данных для команды, в котором мы передаём ключ длиной 6 байтов- собственно данные ключа (мы используем стандартный, который производитель зашил в карту при изготовлении —
FF FF FF FF FF FF
).
Коды ошибок для Load Keys определены в спецификации PC/SC part 3, раздел 3.2.2.1.4, вы должны их все обрабатывать, если пишете нормальную программу. Однако не гарантируется, что терминал вернёт корректную ошибку, часто он возвращает общий код 63 00
без уточнения, что именно пошло не так.
❈ ❈ ❈
Следующий этап — аутентификация, она выполняется для конкретного блока. Инструкция — General Authenticate, она описана в спецификации PC/SC part 3, раздел 3.2.2.1.6.
// 2. authentication
// CLA INS P1 P2 Lc Data
command = toByteArray("FF 86 00 00 05 01 00 00 60 00");
answer = channel.transmit(new CommandAPDU(command));
if (answer.getSW() != 0x9000) {
System.out.printf("Response failed: %s%n", hexify(answer.getBytes()));
throw new InstructionFailedException();
}
APDU команды выглядит так: FF 86 00 00 05 01 00 00 60 00
CLA = FF
INS = 86
, команда General AuthenticateP1 = 00
, должен стоять нольP2 = 00
, должен стоять нольLc = 05
, длина данных для команды, 5 байтов- собственно данные, они имеют следующую структуру
01
— версия, на данный момент только01
00
— старший байт номера блока00
— младший байт номера блока, в нашем случае мы хотим прочитать нулевой блок60
— типа ключа, который мы хотим использовать для аутентификации,60
означает Ключ A,61
— Ключ B.00
— номер ключа, см. вызов предыдущей команды, аргумент P2
На этом этапе терминал может вернуть специальную ошибку в статусном слове, если вы указали на прошлом этапе некорректный ключ. Однако это тоже не гарантируется и вы можете получить опять общий код 63 00
.
Пару слов об адресации блоков. Схема со старшим и младшим байтами позволяет адресовать 65536 блоков с адресами от 0x0000
до 0xFFFF
. В Mifare Classic 1K только 64 блока, поэтому старший байт всегда будет нулевым, а младший может быть в диапазоне от 0x00
до 0x3F
.
❈ ❈ ❈
И, наконец, чтение данных из блока. Мы хотим прочитать тот блок, для которого проводили аутентификацию на прошлом этапе. Инструкция — Read Binary, она описана в спецификации PC/SC part 3, раздел 3.2.2.1.8.
// 3. read data
// CLA INS P1 P2 Le
command = toByteArray("FF B0 00 00 10");
answer = channel.transmit(new CommandAPDU(command));
if (answer.getSW() != 0x9000) {
System.out.printf("Response failed: %s%n", hexify(answer.getBytes()));
throw new InstructionFailedException();
}
System.out.printf("Block data: %s%n", hexify(answer.getData()));
APDU команды выглядит так: FF B0 00 00 10
CLA = FF
INS = B0
, команда Read BinaryP1 = 00
, старший байт номера блокаP2 = 00
, младший байт номера блокаLe = 10
, размер ожидаемого результата, 16 байтов (10
в шестнадцатеричном представлении)
Если мы правильно угадали с ключом, терминал должен вернуть R-APDU с байтами нулевого блока. В случае моей карты это выглядит так:
Using terminal PC/SC terminal ACS ACR122U
Block data: 9C 9C AC 99 35 08 04 00 01 51 BE 34 B3 D0 FB 1D
В этом блоке находится записанная производителем системная информация о карте, например, UID. Теоретически, пользовательно не должен иметь права на её модификацию.
Мы используем метод getData()
чтобы отсечь последние два байта ответа от байтового массива.
Вы можете поэкспериментировать с кодом, например, чтобы вернуть содержимое второго блока (с адресом 0x0001
), отправьте в третьем этапе команду с таким APDU: FF B0 00 01 10
. Обратите внимание, что после успешной аутентификации инструкция Read Binary будет использовать этот же ключ для всех попыток доступа к любому из четырёх блоков этого же сектора. Поэтому вы можете аутентифицироваться для блока 0x0000
и пытаться читать данные из остальных трёх блоков без необходимости проводить аутентификацию заново. Однако читать блоки из другого сектора уже не получится.
В обычных картах (например, транспортных) для хранения данных используется только несколько секторов, а для первого сектора часто оставляется стандартный ключ доступа. Например, в карте «Тройка» Ключ А первого сектора: A0 A1 A2 A3 A4 A5
.
Важные моменты из этого примера
- В четвёртом блоке каждого сектора хранятся ключи (только для этого сектора!) и условия доступа для всех четырёх блоков сектора.
- Всего в секторе можно хранить два ключа: Ключ A и Ключ B.
- Для каждого блока можно установить свои параметры доступа.
- Для любых операций с данными на карте Mifare Classic требуется предварительная аутентификация для конкретного блока, после чего можно без аутентификации получить доступ к остальным блокам сектора (с этим же ключом).
- Аутентификация выполняется в два этапа: сначала ключ загружается в терминал, а затем производится аутентификация со ссылкой на ранее загруженный ключ.
- Ключ живёт в терминале до его отключения, если он был загружен в волатильную память.
- Если загрузить ключ в неволатильную память, он будет доступен и после отключения.
- В ACR122U можно загрузить одновременно только два ключа и только в волатильную память.
- У каждого терминала есть особенности, поэтому обязательно читайте документацию.
- Для новой карты ключи A и B установлены в значения по умолчанию, разные для разных производителей.
example-05: Разбор данных на карте Mifare Classic¶
Для этого примера можно использовать только темринал для бесконтактных чиповых карт.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-05/Example.java
Цели этого примера:
- корректно разобрать условия доступа для каждого блока;
- корректно обработать все ошибки.
По сути этот пример является чуть усложнённой версией предыдущего. Схема программы примерно такая:
- задать список констант-ключей;
- пройтись по всем секторам карты и попробовать аутентифицироваться на каждом всеми заданными ключами;
- попытаться прочитать содержимое всех блоков при успешной аутентификации;
- попытаться прочитать условия доступа для каждого сектора;
- красиво изобразить результат на экране в псевдографике:
- показать для каждого из секторов, какой ключ обнаружен (A, B, A и B, никакой);
- показать данные для каждого блока и ключ, при помощи которого они были прочитаны;
- также показать биты доступа для каждого блока.
❈ ❈ ❈
Для начала определим список ключей, которыми будем тестировать карту:
// define list of keys to test
var defaultKeys = new ArrayList<String>();
defaultKeys.add("FF FF FF FF FF FF"); // default NXP key
defaultKeys.add("A0 A1 A2 A3 A4 A5"); // default Infineon Key A
defaultKeys.add("B0 B1 B2 B3 B4 B5"); // default Infineon Key B
var keysNumber = defaultKeys.size();
И создадим переменные, в которых будем сохранять прочитанные данные по мере их обработки:
// store collected data in these variables
var blocksData = new ArrayList<byte[]>(64);
var blocksKeys = new ArrayList<String>(64);
var blocksAccessBits = new ArrayList<char[]>(64);
В blocksData
будем сохранять байты каждого блока, в blocksKeys
ключ, которым мы прочитали блок; в blocksAccessBits
биты условий доступа.
Так как весь результат мы будем выводить в окно консоли, напечатаем сначала заголовок со списком всех секторов, дальше при чтении карты мы параллельно будем заполнять определённые выше массивы и печатать в консоль информацию по найденным ключам для каждого сектора. После чтения всех секторов распечатаем данные для каждого блока.
Итак, заголовок:
// print header for all sectors
System.out.printf("Sectors: ");
for (int sector=0; sector<16; sector++) {
System.out.printf("0x%X ", sector);
}
Выполним цикл для каждого из 16 секторов. В начале каждой итерации добавим четыре пустых объекта (null) во все три массива. Если мы найдём данные для них дальше, заполним ими вместо null. Определим две boolean-переменные, где будем хранить информацию о найденных ключах A и B. И вычислим «адрес» первого блока в сектора в переменной firstBlock
.
for (int sector=0; sector<16; sector++) {
// insert null object for all blocks in this sector, they
// will be updated later
for (int j=0; j<4; j++) {
blocksData.add(null);
blocksKeys.add(null);
blocksAccessBits.add(null);
}
boolean keyAFound = false;
boolean keyBFound = false;
// calculate first sector block address
int firstBlock = sector * 4;
Так как мы будем выполнять одинаковые APDU запросы много раз, создадим для них базовые массивы, которые будем модифицировать нужным образом при каждой операции:
// General Authenticate APDU template
var authenticateCommand = toByteArray("FF 86 00 00 05 01 00 00 00 00");
// Read Binary APDU template
var readBinaryCommand = toByteArray("FF B0 00 00 10");
Теперь для каждого из ключей выполним такой код: загрузим ключ, попытаемся аутентифицироваться им как ключом B, если получится, то попытаемся прочитать содержимое блока, если получилось, запишем данные в blocksData
и blocksKeys
. Затем повторим всё для ключа A.
for (var key : defaultKeys) {
// sonstruct Load Keys instruction APDU
var loadKeysCommand = toByteArray("FF 82 00 00 06 " + key);
var answer = channel.transmit(new CommandAPDU(loadKeysCommand));
if (answer.getSW() != 0x9000) {
System.out.println("Failed to load keys");
continue;
}
// try to auth using key as Key B for the first block
// prepare authenticateCommand
authenticateCommand[7] = (byte)firstBlock;
authenticateCommand[8] = 0x61;
answer = channel.transmit(new CommandAPDU(authenticateCommand));
if (answer.getSW() == 0x9000) {
keyBFound = true;
// success, try to read data from all blocks (for this sector only!):
for (int i=0; i<4; i++) {
var block = firstBlock + i;
readBinaryCommand[3] = (byte)block;
answer = channel.transmit(new CommandAPDU(readBinaryCommand));
if (answer.getSW() == 0x9000) {
blocksData.set(block, answer.getData());
blocksKeys.set(block, "B: " + key);
}
}
}
// try to auth using key as Key A for the first block
// prepare authenticateCommand
authenticateCommand[7] = (byte)firstBlock;
authenticateCommand[8] = 0x60;
answer = channel.transmit(new CommandAPDU(authenticateCommand));
if (answer.getSW() == 0x9000) {
keyAFound = true;
// success, try to read data from all blocks (for this sector only!):
for (int i=0; i<4; i++) {
var block = firstBlock + i;
readBinaryCommand[3] = (byte)block;
answer = channel.transmit(new CommandAPDU(readBinaryCommand));
if (answer.getSW() == 0x9000) {
blocksData.set(block, answer.getData());
blocksKeys.set(block, "A: " + key);
}
}
}
}
После проверки всех ключей напечатаем статус (он будет печататься в одну строку):
// print found key status for this sector
if (keyAFound && keyBFound) {
System.out.printf("++++ ");
} else if (keyAFound) {
System.out.printf("AAAA ");
} else if (keyBFound) {
System.out.printf("BBBB ");
} else {
System.out.printf("---- ");
}
И теперь попытаемся вычислить биты контроля доступа, если мы ранее прочитали трейлер (то есть четвёртый блок) текущего сектора:
// calculate and store access bits for this sector blocks
var lastBlock = firstBlock + 3;
var trailerBytes = blocksData.get(lastBlock);
if (trailerBytes != null) {
// get access control bytes
byte b7 = trailerBytes[7];
byte b8 = trailerBytes[8];
char[] bits;
bits = new char[3];
bits[0] = checkAccessBit(b7, 4);
bits[1] = checkAccessBit(b8, 0);
bits[2] = checkAccessBit(b8, 4);
blocksAccessBits.set(firstBlock, bits);
bits = new char[3];
bits[0] = checkAccessBit(b7, 5);
bits[1] = checkAccessBit(b8, 1);
bits[2] = checkAccessBit(b8, 5);
blocksAccessBits.set(firstBlock+1, bits);
bits = new char[3];
bits[0] = checkAccessBit(b7, 6);
bits[1] = checkAccessBit(b8, 2);
bits[2] = checkAccessBit(b8, 6);
blocksAccessBits.set(firstBlock+2, bits);
bits = new char[3];
bits[0] = checkAccessBit(b7, 7);
bits[1] = checkAccessBit(b8, 3);
bits[2] = checkAccessBit(b8, 7);
blocksAccessBits.set(firstBlock+3, bits);
}
Здесь мы используем наш метод checkAccessBit
, который возвращает символ '0'
или '1'
в зависимости от того, какой бит стоит в указанной позиции байта. Все биты контроля доступа упакованы в седьмой и восьмой байты трейлера, мы их предварительно вытаскиваем в переменные b7
и b8
и дальше заполняем массив blocksAccessBits
.
Теперь у нас есть все данные и мы их распечатываем в консоли, этот код я не привожу, так как он тривиальный и вы его можете сами посмотреть в исходниках примера: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-05/Example.java (ищите строчку // now print found data
).
Результат выполнения программы на одной из моих карт можно посмотреть тут: https://gist.github.com/sigsergv/16c57a7e709c8af029c676f2f97e7bc0.
❈ ❈ ❈
Для каждого блока в последней колонке таблицы печатаются три бита контроля доступа (или условия доступа) и о них я расскажу подробно.
Информация об условиях доступа записана в трейлере сектора в байтах 6, 7 и 8. И для каждого блока сектора (включая блок с трейлером) она определяется тремя битами, их принято обозначать как C1, C2, C3. Эти биты определённым образом распределены по битовым представлениям байтов 6, 7 и 8. Всего нам нужно 4×3=12 битов, они хранятся в 24 битах (8×3=24) два раза: в прямом и инвертированном видах. Теоретически, при чтении трейлера вы должны проверять целостность, чтобы соответствующие биты на инвертированной и прямой позициях дополняли друг друга, но я здесь этого не делаю.
Сочетания битов (их, как нетрудно понять, 8) имеют разный смысл для обычных блоков (первых трёх) и трейлера. Ниже две таблицы, как их интерпретировать, они взяты (в несколько упрощённом виде) из технической документации к карте Mifare Classic 1K.
Сочетание битов C1, C2 и C3 для первых трёх блоков сектора определяет следующие условия доступа:
C1 C2 C3 │ Что можно делать (остальное нельзя)
═══════════╪════════════════════════════════════════════════════════════════════
0 0 0 │ Разрешены все операции для ключей A или B
0 1 0 │ Read(А,B)
1 0 0 │ Read(A,B), Write(B)
1 1 0 │ Read(A,B), Write(B), Increment(B), Decrement/Transfer/Restore(A,B)
0 0 1 │ Read(A,B), Decrement/Transfer/Restore(A,B)
0 1 1 │ Read(B), Write(B)
1 0 1 │ Read(B)
1 1 1 │ Запрещены все операции для обоих ключей
Здесь запись Read(A,B)
означает, что разрешено чтение блока ключом A или B, Write(B)
— разрешена запись блока только ключом B и так далее. Напомню, что блок в mifare пишется только целиком.
Сочетание битов C1, C2 и C3 для трейлера сектора определяет следующие условия доступа до блока с трейлером:
C1 C2 C3 │ Что можно делать (остальное нельзя)
═══════════╪════════════════════════════════════════════════════════════════════
0 0 0 │ WriteA(A), AccessRead(A), ReadB(A), WriteB(A)
0 1 0 │ AccessRead(A), ReadB(A)
1 0 0 │ WriteA(B), AccessRead(A,B), WriteB(B)
1 1 0 │ AccessRead(A,B)
0 0 1 │ WriteA(A), AccessRead(A), AccessWrite(A), ReadB(A), WriteB(A)
0 1 1 │ WriteA(B), AccessRead(A,B), AccessWrite(B), WriteB(B)
1 0 1 │ AccessRead(A,B), AccessWrite(B)
1 1 1 │ AccessRead(A,B)
Здесь WriteA(B)
означает, что можно ключом B перезаписать ключ A, ReadB(A)
означает, что можно ключом A прочитать ключ B, AccessRead(A)
можно читать байты контроля доступа ключом A и так далее.
Обратите внимание, что использовать Ключ B для контроля доступа можно только при условии, что условия доступа на трейлер не позволяют его прочитать. Ключ A нельзя прочитать никогда. Биты доступа можно прочитать всегда либо ключом A, либо ключом B. Если какой-то из элементов запрещено читать, то в нём возвращаются нули.
При помощи этих таблиц вы можете теперь читать последнюю колонку в результатах работы программы.
Вот пример разбора байтов контроля доступа для блока с трейлером:
Значение битов 0 1 1
смотрим в таблице выше — WriteA(B), AccessRead(A,B), AccessWrite(B), WriteB(B)
. Это означает:
WriteA(B)
— можно записать Ключ A при помощии Ключа B;AccessRead(A,B)
— можно читать байты контроля доступа Ключом A или B;AccessWrite(B)
— можно записывать байты контроля доступом Ключом B;WriteB(B)
— можно записать Ключ B при помощи Ключа B.
Рамкой обведено нужное значение, которое означает:
- Ключ A нельзя читать вообще
- Ключ A можно записать при помощи ключа B
- Условия доступа можно читать при помощи ключа A или B, а писать только при помощи Ключа B
- Ключ B можно читать и писать только при помощи Ключа B.
Важные моменты и замечания из этого примера
- Фактически пользователю доступно 768 байтов из 1024, остальное выделено под служебную информацию.
- Подбор ключей по такой схеме — очень неэффективная операция.
- Неправильной установкой битов доступа можно полностью отрезать себе доступ к блоку.
- Новая «заводская» карта использует стандартные ключи, которые зависят от производителя.
- Некоторые карты снабжены защитой от перебора ключей, для них операция General Auth будет возвращать ошибку в течение некоторого интервала времени, если предыдущая попытка была неудачной. Чтобы это обойти, нужно вставить в код искусственную задержку в пару секунд перед каждой операцией General Auth.
example-06: Персонализация карты Mifare Classic 1K¶
Для этого примера можно использовать только терминал для бесконтактных чиповых карт. Также вам понадобится карта Mifare Classic 1K.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-06/
Цели этого примера:
- планирование структуры размещения данных;
- демонстрация записи данных на карту памяти Mifare Classic 1K:
- запись данных в блоки;
- запись ключей;
- запись условий доступа;
- написать несколько независимых программ для разных ролей.
В самом начале я рассказывал коротко о жизненном цикле смарт-карты. Персонализация — это подготовка карты перед выдачей её клиенту. В этом примере мы напишем код для персонализации, а также для «реального» использования карты. В качестве базовой модели возьмём электронный проездной. А терминал для бесконтактных карт будет исполнять разные роли в зависимости от запущенной программы.
Итак, требования к нашему проездному:
- на карте хранится баланс в виде целого положительного числа;
- терминал в роли «эмитент» может «выпустить» карту:
- в определённый сектор записываются ключи доступа;
- для сектора задаются права доступа на трейлер и первый блок сектора;
- в первый блок записывается нулевой баланс;
- терминал в роли «кассир» может выполнять следующие операции:
- прочитать и показать текущий баланс карты;
- увеличить баланс на определённую величину;
- терминал в роли «кондуктор» может списать фиксированную сумму, то есть уменьшить записанный баланс на некоторое число;
- терминал в роли «эмитент» может «уничтожить» карту, то есть вернуть весь сектор в исходное состояние, удалить записанные данные и выставить начальные ключи и условия доступа.
Для конфигурации всего набора приложений нам нужно отдельно задать несколько параметров.
Во-первых, это номер сектора для хранения наших данных, номер зависит от того, какую карту вы будете использовать для экспериментов. Если это чистая фабричная mifare, то можно оставить значение по умолчанию — второй сектор (то есть с номером 1). Если у вас карта, на которой уже используются секторы для других целей (например, активная транспортная карта), то вам придётся использовать другой сектор. Для определения доступных секторов вы можете воспользоваться прошлым примером.
Во-вторых, это исходные ключи для сектора, по умолчанию используются стандартные ключи NXP: FF FF FF FF FF FF
для обоих ключей A и B.
Структура проекта такая:
- все java-файлы в одном каталоге;
- разные роли терминала инициируются разными файлами, которые должны запускаться по одному;
- для упрощения запуска все команды сведены в проектный Makefile (конкретные команды будут указаны в разделах ниже);
- все общие методы, исключения и классы я вынес в отдельный вспомогательный класс
Util
; - вся конфигурация проекта (ключи, сектор и другие параметры) записываются в файл
project.properties
, он читается в каждой из программ.
IssueCard.java¶
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-06/IssueCard.java
Команда для запуска:
make issue-card
Эта программа для персонализации карты, то есть подготовки чистой (пустой) карты для использования в качестве электронного проездного. В роли чистой может использоваться любая карта, в которой для выбранного нами сектора можно писать все поля (то есть оба ключа A и B, а также условия доступа) в трейлере, при помощи исходного Ключа А. Технически можно этого же можно добиться при помощи Ключа B, но такую проверку я не хочу делать, так как это увеличивает количество кода.
В самом начале мы читаем конфигурацию:
// load project configuration data
var config = Util.loadConfig();
Дальше идут стандартные шаги для подключения к терминалу, инициализация базовых APDU и другие объявления переменных:
var card = terminal.connect("T=1");
var channel = card.getBasicChannel();
var authenticateCommand = Util.toByteArray("FF 86 00 00 05 01 00 00 00 00");
var readBinaryCommand = Util.toByteArray("FF B0 00 00 10");
var updateBinaryCommand = Util.toByteArray("FF D6 00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00");
var firstBlock = (byte)(config.sector * 4);
byte[] data;
byte[] command;
ResponseAPDU answer;
Загружаем Ключ А (который записан в файле конфигурации) в терминал и пытаемся с ним авторизоваться на блоке с трейлером нужного сектора (который тоже берём из конфигурации):
// load Key A to cell 00
var loadKeysCommand = Util.toByteArray("FF 82 00 00 06 " + config.initial_key_a);
answer = channel.transmit(new CommandAPDU(loadKeysCommand));
if (answer.getSW() != 0x9000) {
throw new Util.CardCheckFailedException("Failed to load Key A into terminal.");
}
// check Key A only: authenticate for the trailer block
authenticateCommand[7] = (byte)(firstBlock+3);
authenticateCommand[8] = 0x60;
answer = channel.transmit(new CommandAPDU(authenticateCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Key A doesn't match.");
}
В случае ошибки мы выбрасываем исключение Util.CardCheckFailedException
с поясняющим сообщением.
Дальше читаем содержимое блока с трейлером и проверяем условия доступа на трейлер. Наша цель — иметь возможность полностью перезаписать блок с трейлером, условия доступа на другие секторы нас не интересуют, так как мы их всё равно выставим в нужные.
// read trailer block data
readBinaryCommand[3] = (byte)(firstBlock+3);
answer = channel.transmit(new CommandAPDU(readBinaryCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Failed to read block with Key A.");
}
// read trailer and check that we should be able to set both keys and change access
readBinaryCommand[3] = (byte)(firstBlock+3);
answer = channel.transmit(new CommandAPDU(readBinaryCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Failed to read trailer with Key A.");
}
data = answer.getData();
String[] accessBits = Util.decodeAccessBits(data[6], data[7], data[8]);
if (!accessBits[3].equals("001")) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Access condition bits don't match.");
}
Для чтения условий доступа мы используем метод Util.decodeAccessBits()
, который возвращает биты доступа для всех блоков сектора в виде четырёх строк, каждая из которых состоит из трёх символов 1
или 0
. В метод передаём шестой, седьмой и восьмой байты трейлера, в которых упакованы биты доступа. Реализацию метода можете посмотреть сами в исходном коде модуля.
Если все проверки прошли успешно, мы можем теперь записать нужные данные в сектор карты: в первый блок (с номером 0
) записываем стартовый баланс, в трейлер записываем новые ключи (которые задаются в конфигурации проекта) и новые условия доступа для первого блока и трейлера (остальные блоки оставляем без изменений).
Баланс будем хранить в виде long-значения, представленного в виде фиксированного 8-байтового массива, где старшие байты заполнены нулями. Например, значение 123456
хранится в виде массива 00 00 00 00 00 01 E2 40
. Для преобразования между long
и byte[]
сделаем два метода:
long Util.bytesToLong(byte[])
byte[] Util.longToBytes(long)
Значение будем хранить в первых восьми байтах первого блока.
❈ ❈ ❈
Новая команда, которую мы будем использовать в этом примере — Update Binary, она определяется в разделе 3.2.2.1.9 спецификации PC/SC part 3. У нас в коде мы задали базовую переменную updateBinaryCommand
с таким APDU:
CLA INS P1 P2 Lc DATA
FF D6 00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
CLA = FF
INS = D6
, команда Update BinaryP1 = 00
, старший байт номера блокаP2 = 00
, младший байт номера блока, он будет задан при копировании базовой переменной в конкретную командуLс = 10
, размер блока данных далее, 16 байтов (10
в шестнадцатеричном представлении)DATA = ...
, 16 байтов нулей, они также будут заменены на реальные байты при копировании базовой переменной
Сначала запишем пустой баланс в первый сектор, это будет массив data
из 16 байтов-нулей, дальше склонируем переменную updateBinaryCommand
в новый массив, обновим его адресом первого блока и набором байтов и выполним команду.
// store empty balance to first block
data = new byte[16];
// create APDU by cloning updateBinaryCommand template, specify target
// block address and copy data block
command = updateBinaryCommand.clone();
command[3] = firstBlock;
for (int i=0; i<16; i++) {
command[5+i] = data[i];
}
answer = channel.transmit(new CommandAPDU(command));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardUpdateFailedException("Failed to update data block.");
}
Теперь нам нужно сформировать корректный блок трейлера и записать его. Это нужно делать очень аккуратно, так как неверно указанные данные могут полностью лишить вас доступа к этому блоку. Напомню структуру байтов трейлера:
KEY A ACCESS BYTES USER BYTE KEY B
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Создадим новый байтовый массив и заполним его ключами из конфигурации, также выставим пользовательский байт (USER BYTE) в 0xFF
, чтобы он явно выделялся в этом массиве:
data = new byte[16];
// fill with new Key A and Key B
var prodKeyA = Util.toByteArray(config.prod_key_a);
var prodKeyB = Util.toByteArray(config.prod_key_b);
for (int i=0; i<6; i++) {
data[i] = prodKeyA[i];
data[10+i] = prodKeyB[i];
}
// force set user byte to 0xFF
data[9] = (byte)0xFF;
После этих манипуляций буфер data
выглядит так:
81 82 83 84 85 86 00 00 00 FF 91 92 93 94 95 96
Три нулевых байта в середине нужно заполнить корректными условиями доступа. Напишем новый метод byte[] encodeAccessBits(String[])
, он на вход принимает четыре строки с битами (типа "010"
) для каждого из четырёх блоков, а возвращает три байта с упакованными значениями.
Но сначала нужно определиться, какие именно разрешения для каких ключей хотим дать на карту. Рассмотрим такие ограничения:
- «эмитент» знает Ключ А должен уметь писать в трейлер, а также читать и писать из блока с балансом;
- «кассир» и «кондуктор» знают Ключ B и должны уметь читать и писать блок с балансом, доступа к трейлеру у них нет.
Из таблицы битов условий доступа выбираем подходящий вариант для первого блока с балансом:
0 0 0 │ Разрешены все операции для ключей A или B
Для второго и третьего блока сектора запретим все операции для всех ключей, подходящий вариант битов:
1 1 1 │ Запрещены все операции для обоих ключей
А из таблицы битов условий доступа для трейлера выбираем такой вариант:
0 0 1 │ WriteA(A), AccessRead(A), AccessWrite(A), ReadB(A), WriteB(A)
В итоге получается код:
// calculate access conditions bytes
String[] accessConditionBits = {"000", "111", "111", "001"};
var ac = Util.encodeAccessBits(accessConditionBits);
data[6] = ac[0];
data[7] = ac[1];
data[8] = ac[2];
Теперь осталось только записать блок на карту и тем самым завершить персонализацию карты:
// create APDU by cloning updateBinaryCommand template, specify target
// block address (trailer) and copy data block
command = updateBinaryCommand.clone();
command[3] = (byte)(firstBlock+3);
for (int i=0; i<16; i++) {
command[5+i] = data[i];
}
answer = channel.transmit(new CommandAPDU(command));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardUpdateFailedException("Failed to update data block.");
}
CheckBalance.java¶
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-06/CheckBalance.java
Команда для запуска:
make check-balance
Эта программа проверяет баланс на карте. Для её работы нужна персонифицированная на прошлом этапе карта.
Здесь код совсем простой: аутентифицируемся через Ключ B (обратите внимание на authenticateCommand[8] = 0x61;
, а также на то, откуда берём ключ — config.prod_key_b
), читаем первый блок сектора, берём из него первые восемь байтов, преобразуем в значение типа long, печатаем на экране и выходим.
// load production Key B to cell 00
var loadKeysCommand = Util.toByteArray("FF 82 00 00 06 " + config.prod_key_b);
answer = channel.transmit(new CommandAPDU(loadKeysCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Failed to load Key B into terminal.");
}
// authenticate using Key B
authenticateCommand[7] = firstBlock;
authenticateCommand[8] = 0x61;
answer = channel.transmit(new CommandAPDU(authenticateCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Key B doesn't match.");
}
// read balance block data
readBinaryCommand[3] = firstBlock;
answer = channel.transmit(new CommandAPDU(readBinaryCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Failed to read block with Key A.");
}
// take first 8 bytes
data = answer.getData();
data = copyOfRange(data, 0, 8);
long balance = Util.bytesToLong(data);
System.out.printf("Card balance is: %d%n", balance);
Как упражнение можете дописать программу так, чтобы она работала в цикле, то есть не завершалась после прочтения баланса, а ждала следующую карту. В этом вам поможет метод javax.smartcardio.CardTerminal.waitForCardAbsent.
TopUpBalance.java¶
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-06/TopUpBalance.java
Команда для запуска (чтобы добавить на карту 1050 единиц):
make top-up-balance ADD=1050
Альтернативная команда:
java TopUpBalance 1050
Эта программа пополняет баланс на карте и добавляет указанное количество единиц, значение передаётся единственным аргументом при запуске.
Вначале чтение и проверка входных данных, количество добавляемых единиц сохраняем в переменной funds
:
if (args.length != 1) {
System.out.println("Amount of funds is required.");
System.exit(1);
}
int funds = 0;
try {
funds = Integer.decode(args[0]);
} catch (NumberFormatException e) {
System.out.println("Incorrect amount specified.");
System.exit(1);
}
Дальше то же, что и в CheckBalance.java
— мы должны сначала запросить баланс, увеличить его на величину funds
и записать назад на карту. Запись делается таким кодом:
// create APDU by cloning updateBinaryCommand template, specify target
// block address and copy data block
var newBalanceBytes = Util.longToBytes(newBalance);
data = new byte[16];
for (int i=0; i<8; i++) {
data[i] = newBalanceBytes[i];
}
command = updateBinaryCommand.clone();
command[3] = firstBlock;
for (int i=0; i<16; i++) {
command[5+i] = data[i];
}
answer = channel.transmit(new CommandAPDU(command));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardUpdateFailedException("Failed to update data block.");
}
System.out.printf("New balance is: %d%n", newBalance);
Преобразуем значение newBalance
в байтовый массив, расширяем до 16 байтов и записываем в первый блок сектора. И печатаем новый баланс перед отключением.
Работает программа так:
- убираем карты с терминала;
- запускаем командой
java TopUpBalance 100
, чтобы пополнить на 100 единиц; - подносим карту к терминалу, программа печатает статус и завершается;
- если всё прошло успешно, то баланс на карте увеличивается на 100 единиц.
Checkout.java¶
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-06/Checkout.java
Команда для запуска:
make checkout
Эта программа запускает терминал в роли «кондуктора»: каждое прикосновение картой «списывает» с неё фиксированную сумма, которая задаётся в свойства проекта (ticket_price
в файле project.properties).
Этот код практически не отличается от TopUpBalance.java
, только работает в другую сторону — уменьшает баланс, а не увеличивает. Но пару моментов отмечу. Самый главный — программа работает в таком цикле:
while (true) {
System.out.printf("Waiting for card... ");
terminal.waitForCardPresent(0);
try {
var card = terminal.connect("T=1");
var channel = card.getBasicChannel();
........ здесь код работы с данными .........
} catch (Util.CardCheckFailedException e) {
System.out.printf("failed, please remove card%n");
System.out.printf("Error: %s%n", e.getMessage());
} catch (Util.CardUpdateFailedException e) {
System.out.printf("failed%n");
System.out.printf("Error: %s%n", e.getMessage());
} catch (CardException e) {
System.out.println("CardException: " + e.toString());
System.exit(2);
}
terminal.waitForCardAbsent(0);
}
В начале тела цикла мы вызываем метод terminal.waitForCardPresent(0);
, который ждёт появления карты в терминале, а после завершения всех действий мы вызываем terminal.waitForCardAbsent(0);
, который ждёт, когда карту из терминала уберут.
Количество денег, которое списывается при каждом прикосновении карты к терминалу, задаётся в параметре ticket_price
и в коде доступно через config.ticket_price
.
В терминале лог работы программы выглядит примерно так:
Checkout terminal
=================
Waiting for card... success, new balance: 580, please remove card
Waiting for card... success, new balance: 426, please remove card
Waiting for card... success, new balance: 272, please remove card
Waiting for card... success, new balance: 118, please remove card
Waiting for card... not enough funds: 118
Waiting for card... not enough funds: 118
Waiting for card... ^C
Программа работает до тех пор, пока мы не прервём её через Ctrl+C.
RevokeCard.java¶
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-06/RevokeCard.java
Команда для запуска:
make revoke-card
И последняя программа этого примера, она очищает сектор, который мы использовали для работы. Вот конкретные шаги:
- аутентифицируемся по Ключу А (
config.prod_key_a
); - записываем нули в первый блок сектора;
- вычисляем «исходные» условия доступа (в битах для блоков сектора:
000 000 000 001
); - записываем в трейлер исходные Ключ A, Ключ B и условия доступа.
Приводить здесь код я не буду, он тривиальный.
❈ ❈ ❈
Весь этот пример достаточно простой, но он достоверно демонстрирует принципы работы с данными на карте Mifare Classic 1K. А вот несколько идей, как можно его расширить:
- разрешить списывание средств не чаще чем каждые пять минут, для этого нужно в секторе сохранять время последнего списывания средств и сравнивать его с текущим;
- записывать на карте идентификатор (номер) клиента;
- записывать на карте и проверять срок действия карты.
Работа с контактными картами памяти¶
Контактные карты памяти не имеют внутренней программной логики (как и карты 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-07: Работа с контактной картой памяти SLE 5542¶
Для этого примера можно использовать только терминал ACR38U-I1 (или другой этого же производителя с той же системой команд) и карту памяти SLE5542/SLE4442.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-07/
Цели этого примера:
- показать, как читать и писать на контактную карту памяти через терминал;
- показать, как правильно читать спецификации карты и терминала.
Прежде всего вам понадобятся два документа, с которыми вы будете постоянно сверяться:
- SLE 4442 datasheet (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. Часть из этих полей защищена от перезаписи производителем чипа.
ReadCard.java¶
Исходный код программы: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-07/ReadCard.java
Команда для запуска:
make read
В этой программе мы прочитаем содержимое всех 256 байтов EEPROM, содержимое Protection memory и счётчик ошибок авторизации.
Для начала мы должны сообщить терминалу, какую именно карту мы хотим использовать. Это делает инструкция, описанная в [ACR] в разделе 9.3.6.1. SELECT_CARD_TYPE:
// Instruction "9.3.6.1. SELECT_CARD_TYPE"
// write 1 (in "Lc" field) byte, 06 (in DATA block) indicates card type
// fields "P1" and "P2" are ignored
// INS P1 P2 Lc DATA
var selectCommand = Util.toByteArray("FF A4 00 00 01 06");
answer = channel.transmit(new CommandAPDU(selectCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Select failed");
}
Эта инструкция ОБЯЗАТЕЛЬНА при работе с картой, если её не выполнить, результат будет непредсказуемым.
❈ ❈ ❈
Псевдо-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
// INS P1 P2 Le
var readCardCommand = Util.toByteArray("FF B0 00 00 20");
answer = channel.transmit(new CommandAPDU(readCardCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Cannot read card data");
}
var EEPROMData1 = answer.getData();
// Then read remaining 224=0xE0 (in "Le" field) bytes starting with address 0x20 (in "P2" field)
// field "P1" is ignored
// INS P1 P2 Le
readCardCommand = Util.toByteArray("FF B0 00 20 E0");
answer = channel.transmit(new CommandAPDU(readCardCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Cannot read card data");
}
var EEPROMData2 = answer.getData();
var EEPROMData = new byte[256];
for (int i=0; i<32; i++) {
EEPROMData[i] = EEPROMData1[i];
}
for (int i=0; i<224; i++) {
EEPROMData[32+i] = EEPROMData2[i];
}
Распечатаем все байты блоками по 32:
System.out.printf("EEPROM memory:%n");
for (int i=0; i<8; i++) {
for (int j=0; j<32; j++) {
int addr = i*32 + j;
System.out.printf("%02X ", EEPROMData[addr]);
}
System.out.printf("%n");
}
❈ ❈ ❈
Чтение содержимого 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
// INS P1 P2 Le
var readPROMCommand = Util.toByteArray("FF B2 00 00 04");
answer = channel.transmit(new CommandAPDU(readPROMCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Cannot read protection memory data");
}
var PRBData = answer.getData();
Однако мы хотим не просто показать прочитанные байты, но сделать это в виде таблицы, чтобы показать, какой из первых тридцати двух байтов 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
— можно):
System.out.printf("Protection memory bits:%n");
for (int i=0; i<32; i++) {
System.out.printf("%02d ", i);
}
System.out.printf("%n");
var protectionBits = new int[32];
for (int k=0; k<4; k++) {
var b = PRBData[k];
for (int i=0; i<8; i++) {
var addr = k*8 + i;
protectionBits[addr] = (b & 1);
b >>= 1;
}
}
for (int i=0; i<32; i++) {
System.out.printf(" %d ", protectionBits[i]);
}
System.out.printf("%n");
❈ ❈ ❈
Также распечатаем значение счётчика ошибок, эта команда описана в разделе 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
// INS P1 P2 Le
var readECCommand = Util.toByteArray("FF B1 00 00 04");
answer = channel.transmit(new CommandAPDU(readECCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Cannot read security memory data");
}
var ECData = answer.getData();
System.out.printf("EC: %02X%n", ECData[0]);
Значение 0x07 означает, что предыдущая попытка авторизации завершилась успешно.
❈ ❈ ❈
Вот как выглядит запуск этой программы у меня на новой чистой карте:
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 55
Protection memory bits:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
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, тип карты).
WriteCard.java¶
Исходный код программы: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-07/WriteCard.java
Команда для запуска:
make 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
// INS P1 P2 Lc Data
var presentPSCCommand = Util.toByteArray("FF 20 00 00 03 FF FF FF");
answer = channel.transmit(new CommandAPDU(presentPSCCommand));
if (answer.getSW() != 0x9007) {
card.disconnect(false);
throw new Util.CardCheckFailedException("PSC auth failed");
}
Важный момент: мы сравниваем статусное слово (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
// INS P1 P2 Lc
var writeCommand = Util.toByteArray("FF D0 00 40 04 01 02 03 04");
answer = channel.transmit(new CommandAPDU(writeCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardCheckFailedException("Write command failed");
}
Если мы теперь прочитаем карту предыдущей программой, то увидим записанные нами байты.
❈ ❈ ❈
На этом месте я пример завершаю. За кадром остались такие инструкции:
- 9.3.6.6. WRITE_PROTECTION_MEMORY_CARD — защита определённых байтов изменением Protection memory;
- 9.3.6.8. CHANGE_CODE_MEMORY_CARD (SLE 4442 and SLE 5542) — изменение пин-кода PSC.
Вы можете с ними поэкспериментировать самостоятельно.
example-08: Приложение для отправки APDU¶
Для этого примера можно использовать оба типа терминалов: для бесконтактных и контактных чиповых карт.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-08/
Цели этого примера:
- написать простое консольное приложение для интерактивной работы с картой:
- отправка введённых C-APDU;
- проверка входных данных;
- отображение ответного R-APDU;
- обработка ошибок терминала;
- использовать дальше это приложение для интерактивных экспериментов с картой.
Приложение предельно простое и представляет собой по сути один цикл, в котором бесконечно можно вызывать channel.transmit
. Также я для него переписал метод toByteArray()
из файла Util.java, который парсит строку в байтовый массив. Он теперь выбрасывает исключение на некорректных входных данных.
while (true) {
System.out.print("C-APDU> ");
var rawAPDU = System.console().readLine();
if (rawAPDU.equals("quit") || rawAPDU.equals("exit")) {
break;
}
byte[] apduBytes = null;
try {
apduBytes = Util.toByteArray(rawAPDU);
} catch (Util.ByteStringParseException e) {
System.out.printf("ERROR: Incorrect input string%n");
}
if (apduBytes == null) {
continue;
}
if (apduBytes.length < 4) {
System.out.printf("ERROR: apdu must be at least 4 bytes long%n");
continue;
}
System.out.printf(">>> %s%n", Util.hexify(apduBytes));
var apdu = new CommandAPDU(apduBytes);
try {
var answer = channel.transmit(apdu);
System.out.printf("<<< %s%n", Util.hexify(answer.getBytes()));
} catch (CardException e) {
System.out.printf("CARD EXCEPTION: %s%n", e.toString());
continue;
}
}
Программа работает в терминале в диалоговом режиме: пользователь вводит APDU в виде hex-строки (FF A4 00 00 01 06
или FFA400000106
), программа парсит её в байтовый массив, отправляет в терминал и печатает ответ. Выглядит это примерно так:
Using terminal PC/SC terminal ACS ACR38U-CCID
Type quit or exit to stop the program.
C-APDU> FF A4 00 00 01 06
>>> FF A4 00 00 01 06
<<< 90 00
C-APDU> FF B0 00 00 10
>>> FF B0 00 00 10
<<< A2 13 10 91 FF FF 81 15 FF FF FF FF FF FF FF FF 90 00
C-APDU>
Для завершения нужно ввести quit
или exit
.
Теория: отраслевые стандарты¶
Ранее я уже неоднократно упоминал международные технические стандарты, которым должны соответствовать смарт-карты. Сейчас я об этом расскажу подробнее.
Самым главным стандартом является семейство 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 определяет физические характеристики, частоты, сигнальный интерфейс, протокол передачи данных для бесконтактных карт. Он состоит из четырёх частей и все они официально переведены на русский язык.
- ISO/IEC 14443-1:2016 Part 1: Physical characteristics
- ISO/IEC 14443-2:2016 Part 2: Radio frequency power and signal interface
- ISO/IEC 14443-3:2016 Part 3: Initialization and anticollision
- ISO/IEC 14443-4:2016 Part 4: Transmission protocol
И переводы:
- ГОСТ Р ИСО/МЭК 14443-1-2013 Карты идентификационные. Карты на интегральных схемах бесконтактные. Карты близкого действия. Часть 1. Физические характеристики
- ГОСТ Р ИСО/МЭК 14443-2-2014 Карты идентификационные. Карты на интегральных схемах бесконтактные. Карты близкого действия. Часть 2. Радиочастотный энергетический и сигнальный интерфейс
- ГОСТ Р ИСО/МЭК 14443-3-2014 Карты идентификационные. Карты на интегральных схемах бесконтактные. Карты близкого действия. Часть 3. Инициализация и антиколлизия
- ГОСТ Р ИСО/МЭК 14443-4-2014 Карты идентификационные. Карты на интегральных схемах бесконтактные. Карты близкого действия. Часть 4. Протокол передачи
❈ ❈ ❈
Near Field Communication — NFC. Эта технология базируется на ISO/IEC 14443 и ISO/IEC 18000-3. ISO/IEC 18000 — это стандарт для RFID-меток, а его часть 3 описывает технические параметры радиоинтерфейса на частоте 13,56 МГц.
❈ ❈ ❈
Банковские карты используют стандарт 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).
Все эти карты сделаны и функционируют в соответствии со стандартами семейства ISO/IEC 7816, в частности, позволяют использовать описанные в ISO/IEC 7816-4 APDU для коммуникации.
Главным терминалом для этого и последующих разделов будет считыватель карт с контактной площадкой. Соответственно нам понадобятся для экспериментов разнообразные чиповые карты: кредитки, сим-карты и так далее.
Работа с данными¶
К сожалению, не существует универсального метода для доступа к данными на смарт-карте. Карты разных типов требуют разных подходов. Самые распространённые на сегодняшний день типы микропроцессорных смарт-карт: чиповые банковские пластиковые карты и SIM-карты для GSM-телефонов.
Главный технический стандарт для банковских карт называется EMV, по первым буквам Europay, MasterCard, Visa. Документация по нему доступна для свободного скачивания с официального сайта, на момент написания статьи последняя версия EMV 4.3 и скачать файлы можно отсюда: https://www.emvco.com/emv-technologies/contact/.
Современное название смарт-карт для GSM/UMTS — UICC (Universal Integrated Circuit Card), изначально назывались SIM-картами, но позднее функциональность существенно расширилась. Развитием стандарта занимается ETSI (European Telecommunications Standards Institute), документы свободно доступны с официального сайта организации: http://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.1 Command-response pairs стандарта ISO/IEC 7816-4. Коротко я про него уже рассказывал: это набор байтов, состоящий из четырёх обязательных байтов заголовка (CLA, INS, P1, P2), и нескольких опциональных (Le, Lc, DATA), при этом все виды C-APDU можно разбить на 4 варианта: Case 1, Case 2, Case 3, Case 4. Каждый C-APDU состоит из как минимум четырёх байтов: CLA INS P1 P2
.
Старший бит b8 в CLA
определяет принадлежность класса. Если b8=0
(то есть значения от 00
до 7F
включительно), то это межотраслевой (interindustry) класс и его дальнейшая структура описывается в стандарте ISO/IEC 7816. Если b8=1
(то есть значения от 80
до FF
включительно), то это проприетарный (proprietary) класс и его структура может описываться где-то ещё, в других стандартах или спецификациях. Для межотраслевого класса остальные биты в байте CLA
задают разные функции (например, логический канал или шифрование данных в команде), которые детально описаны в стандарте. По умолчанию можно байт CLA
всегда выставлять в 00
, а если там предполагается какое-то иное значение, это будет специально указано.
Каждая отрасль определяет собственные команды и форматы данных, обычно для таких инструкций используется проприетарный класс команд CLA
. Например, для банковских карт используется CLA=80
, а для SIM/UICC — CLA=A0
❈ ❈ ❈
В общем случае взаимодействие внешней программы с картой через терминал можно представить как диалог из 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 7816-4 как «главный» файл в иерархии. MF может отсутствовать, но если он есть, то его идентификатор —
3F 00
. - DF name (имя DF)
- DF можно назначить имя — байтовый массив размером до 16 байтов. В имени можно использовать любые байты, а не только ASCII-буквы. Имя должно быть уникально в пределах всей карты.
- file identifier (идентификатор файла) / FID
- Байтовый массив из двух элементов. Идентификаторы файлов, у которых общий родитель, должны быть разными. Идентификатор файла и его родителя должны быть разными. Глобально уникальными в пределах всей карты идентификаторы быть не обязаны.
Такую структуру 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 организации VIDA), а PIX=20 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 описана в разделе 7.1.1 SELECT command ISO/IEC 7816-4, а процедура и логика процесса — в разделе 5.3.1 Structure selection.
SELECT «привязывает» файл к указанному в байте
CLA
каналу, обычно это нулевой, но есть карты, поддерживающие несколько каналов. Номер канала задаётся битами b1 и b2 вCLA
. Однако это уже продвинутый топик и в реальности эта информация вам не пригодится, так как все используют канал по умолчанию.
Байт INS
— A4
. Семантика параметров 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 описывается в разделе 5.3.3 стандарта ISO/IEC 7816-4. Подробно об этих объектах я расскажу в последующих разделах и примерах кода.
Однако SELECT может возвращать данные и в другом формате, отличающемся от описанного в ISO/IEC 7816-4.
❈ ❈ ❈
После выбора EF (про DF чуть позднее) с ним можно работать: читать, писать, удалять. Данные в файле могут быть в одной из трёх форм (русскоязычная терминология даётся по официальному переводу ГОСТ Р ИСО:МЭК 7816-4):
- data units (единицы данных), раздел 7.2.1 ISO/IEC 7816-4
- records (записи), раздел 7.3.1 ISO/IEC 7816-4
- data objects (информационные объекты), раздел 5.2 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.
SIMPLE-TLV¶
Рассмотрим сначала более простой метод SIMPLE-TLV, закодированные в нём данные состоят из двух или трёх полей.
Сначала идёт поле с тегом длиной в один байт, в нём хранится значение от 01
до FE
(00
и FF
не допускаются), которое является номером тега (tag number).
Затем идёт один или три байта, задающих длину блока данных. Если первый байт из них имеет значение от 00
до FE
включительно, то это и есть длина блока данных. Если первый байт имеет значение FF
, то длину определяют следующие два байта, задающие число в формате big endian от 0 до 65535.
И собственно поле с байтовыми данными указанной длины. Вот примеры закодированных таким способом данных (лишние пробелы я расставил для читабельности):
84 07 A0 00 00 02 77 10 10
— номер тега:84
, длина: 7 байтов, данные:A0 00 00 02 77 10 10
21 00
— есть только тег с номером21
, длина блока данных нулевая
Этот тип кодирования не определяет структуру блока с данными, для него это просто набор байтов неизвестной структуры. Другими словами, парсер не сможет автоматически понять, что данные тоже закодированы.
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 78161 0
обозначает контекстно-зависимый класс, эти теги определены в стандарте в ISO/IEC 78161 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 от 00 до 1E).
1 1 1 1 1
означает, что тег продолжается в следующем или нескольких следующих байтах и в подсчётах номера тега не участвует. В каждом последующем байте значение бита b8=1 означает, что этот байт входит в тег. Значение b8=0 означает, что этот байт последний в теге. Биты с b7 по b1 конкатенируются по всем участвующим байтам и формируют номер тега.
ISO/IEC 7816 допускает максимум три байта для тега, бо́льшее количество оставлено на будущее. Тремя байтами можно закодировать номера тегов с 0 до 16383 (hex 00 — 3F FF).
Это очень важный момент: в отличие от SIMPLE-TLV, где в байтах тега записан только номер тега, в BER-TLV в этом поле записаны тип кодирования данных, номер тега и класс тега; эта тройка определяет тип данных. Но для практического использования это всё знать не нужно, достаточно только корректно разобрать бинарные данные, выделить тег и дальше смотреть в стандартах, какие данные должны лежать в блоке данных этого тега.
Вот два примера: теги 6F
и 9F38
.
❈ ❈ ❈
Дальше идут байты, определяющие длину блока данных.
Если бит 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 описан в разделе 5.2.2 стандарта ISO/IEC 7816-4, а также в разделе 8.1.2 стандарта ASN.1 ISO/IEC 8825-1 (он же X.680), скачать актуальную на текущий момент версию можно с официального сайта: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-X.680-201508-I!!PDF-E&type=items.
example-09: Чтение данных на примере банковской карты¶
Для этого примера можно использовать оба типа терминалов: для бесконтактных и контактных чиповых карт. Также понадобится любая банковская карта с чипом.
Код примера: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-09/Example.java
Цели этого примера:
- показать, как кодируются и декодируются данные в BER-TLV;
- продемонстрировать, как выглядит работа с файлами на карте по стандарту ISO/IEC 7816-4;
- показать, как работать со стандартом EMV.
Сразу замечание. Если попытаетесь выполнить инструкции из этого примера на карте памяти (например, SLE4442 или Mifare Classic), то получите ошибку на уровне библиотеки, так как карта таких команд просто не понимает.
Общие принципы работы с банковской картой¶
Банковские карты подчиняются стандарту EMV, его полная спецификация доступна по адресу https://www.emvco.com/emv-technologies/contact/. Все документы разбиты на несколько книг (books), освещающих разные аспекты стандарта.
Процесс работы терминала (IFD, это может быть банкомат или POS-терминал в магазине) с банковской картой (ICC) состоит из нескольких этапов:
- ICC вставляется в IFD, IFD распознаёт подключение и активирует электрические контакты.
- Происходит операция RESET для ICC, IFD получает из карты параметры карты и устанавливает соединение.
- Выполняется транзакция (или несколько транзакций).
- Карта отключается от контактов и вынимается из терминала.
Так как мы работает внутри инфраструктуры PC/SC, то пункты 1 и 2 происходит внутри терминала без нашего участия, примерно так же с пунктом 4. Остаётся пункт 3.
Пункт 3 разбивается на такие этапы:
- Выбор приложения (application) — описывается в EMV_4.3, книга 1, раздел 12 Application Selection.
- Выполнение команд транзакций — описывается в EMV_4.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.
Поиск ADF через PSE¶
C-APDU для выбора DF с именем 1PAY.SYS.DDF01
выглядит так (команда SELECT, ISO/IEC 7816-4, раздел 7.1.1 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
означает (см. Таблицу 40 в ISO/IEC 7816-4), что мы хотим получить ответ в виде шаблона FCI (FCI template) для первого или единственного результата.
Ранее для нас любой результат в статусном слове (SW) помимо 0x9000
рассматривался как ошибка, но в этот раз мы должны также обработать значение 0x6A82
, что в контексте EMV означает Файл PSE не найден. В этом случае мы попробуем найти нужное приложение перебором заранее известных идентификатов.
// Select PSE first
// CLA INS P1 P2 Lc DATA
var selectPSECommand = Util.toByteArray("00 A4 04 00 0E 31 50 41 59 2E 53 59 53 2E 44 44 46 30 31");
answer = channel.transmit(new CommandAPDU(selectPSECommand));
sw = answer.getSW();
byte[] aid = null;
if (sw == 0x9000) {
// take AID from FCI
var PSEData = answer.getData();
aid = getAIDFromPSEFCI(channel, PSEData);
} else if (sw == 0x6A82) {
// guess AID
aid = guessAID(channel);
} else {
throw new Util.CardOperationFailedException(String.format("PSE retrieval failed: 0x%04X", answer.getSW()));
}
В случае успешного завершения вызовем функцию getAIDFromPSEFCI()
, а в случае отсутствующего PSE — функцию guessAID()
. Обе возвращают первый найденный AID, либо null, если ни один не найден.
Разбор FCI template¶
Результатом выполнения инструкции SELECT являются данные, соответствующие FCI template, этот шаблон описан в разделе 5.3.3 File control information стандарта 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
.
Я написал класс, который разбирает переданный ему массив байтов и формирует рекурсивную структуру с разобранными данными. Исходный код вы можете посмотреть сами, он достаточно простой. Я буду дальше этим классом пользоваться в других примерах, однако если вы планируете писать программы, лучше возьмите что-то более качественное, например, https://github.com/evsinev/ber-tlv.
❈ ❈ ❈
Начнём с функции getAIDFromPSEFCI()
, целиком её код — в Example.java, а здесь в тексте только важные фрагменты с подробными комментариями.
На входе байтовый массив с FCI, его структура описана в ISO/IEC 7816-4, раздел 5.3.3 File control information. Это BER-TLV объект с тегом 6F
, он содержит другие объекты с разными тегами, но нужная нам информация хранится в блоке с тегом A5
, это специальный тег для проприетарных данных, структура которого определяется каким-то другим стандартом (а не ISO/IEC 7816). В нашем случае это стандарт EMV_v4.3, книга 1, раздел 11.3.4 Data Field Returned in the Response Message. Вот как мы запрашиваем вложенный объект с тегом A5
:
var root = BerTlv.parseBytes(data);
// pi means "proprietary information"
var piTlv = root.getPart("A5");
if (piTlv == null) {
throw new Util.CardOperationFailedException("Cannot find EMV block in PSE FCI");
}
В объекте piTlv
из всех возможных полей нас интересует поле с тегом 88
, это контекстно-зависимый класс, взятый из ISO/IEC 7816-4. Тег помечает Short EF identifier (SFI) и данный объект содержит короткий идентификатор EF, это число в диапазоне от 1 до 30 включительно, идентифицирующее какой-то конкретный файл в границах карты. По SFI с этим файлом можно выполнять отдельные операции без необходимости предварительного выбора файла через инструкцию SELECT. В нашей ситуации в этом файле содержится SFI Payment System Directory (PSD), то есть каталог всех платёжных систем на карте.
Вот как мы читаем значение SFI:
// piTlv now contains data specified in EMV_v4.3 book 1 spec,
// section "11.3.4 Data Field Returned in the Response Message"
var sfiTlv = piTlv.getPart("88");
if (sfiTlv == null) {
throw new Util.CardOperationFailedException("Cannot find SFI block in PSE FCI");
}
var defSfiData = sfiTlv.getValue();
int sfi = defSfiData[0];
Теперь мы можем прочитать PSD. Это линейный файл с записями (linear record structure, ISO/IEC 7816-4, раздел 5.3.2 Data referencing methods). Мы должны прочитать все записи из файла, это делается инструкцией READ RECORD из ISO/IEC 7816-4, раздел 7.3.3 READ RECORD (S) command. Базовый APDU этой команды в коде задаётся так:
// CLA INS P1 P2 Le
var readRecordCommand = Util.toByteArray("00 B2 00 00 00");
Но сначала мы его должны изменить аргумент P2, чтобы указать там в разных битовых фрагментах (детали описаны в ISO/IEC 7816-4, таблицы 48 и 49) следующее:
- файл по его SFI, а не текущий выбранный файл (записать SFI в старшие пять битов P2);
- прочитать запись по номеру из P1 (записать биты
1 0 0
в младшие три бита P2).
// read single record specified in P1 from EF with short EF identifier sfi
byte p2 = (byte)((sfi << 3) | 4);
readRecordCommand[3] = p2;
Теперь будем последовательно выполнять эту инструкцию с увеличивающимся номером записи, начиная с 1. Здесь есть нюанс: в APDU мы указали байт Le=0
, что означает вернуть все данные сразу, однако в некоторых картах команда может вернуть ошибку 6CXX
, означающую, что нужно повторить запрос, только вместо Le=0
указать точную длину Le=XX
, здесь XX
— это второй байт из ошибки.
Структура каждой записи определена в EMV_v4.3, книга 1, раздел 12.2.3 Coding of a Payment System Directory: объект с тегом 70
, внутри несколько объектов с тегом 61
, внутри которого в поле с тегом 4F
лежит имя приложения платёжной системы (Application Definition File name, ADF name), оно нам и нужно.
ArrayList<byte[]> aids = new ArrayList<byte[]>();
byte recordNumber = 1;
byte expectedLength = 0;
while (true) {
readRecordCommand[2] = recordNumber;
readRecordCommand[4] = expectedLength;
answer = channel.transmit(new CommandAPDU(readRecordCommand));
if (answer.getSW1() == 0x6C) {
expectedLength = (byte)answer.getSW2();
continue;
}
if (answer.getSW() != 0x9000) {
break;
}
var record = answer.getData();
if (record.length != 0) {
BerTlv psd = BerTlv.parseBytes(record);
// psd must have tag "70"
// see EMV_v4.3 book 1, section "12.2.3 Coding of a Payment System Directory"
if (!psd.tagEquals("70")) {
throw new Util.CardOperationFailedException("Cannot find PSD record");
}
for (BerTlv p : psd.getParts()) {
if (p.tagEquals("61")) {
BerTlv aidTlv = p.getPart("4F");
aids.add(aidTlv.getValue());
}
}
}
recordNumber++;
}
Описанный здесь подход разбора шаблона категорически нельзя использовать в реальном промышленном коде. Суть 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 с таким именем.
private final static byte[] guessAID(CardChannel channel)
throws CardException
{
var candidateAIDs = new ArrayList<byte[]>(5);
candidateAIDs.add(Util.toByteArray("A0 00 00 00 03 20 10")); // Visa Electron
candidateAIDs.add(Util.toByteArray("A0 00 00 00 03 10 10")); // Visa Classic
candidateAIDs.add(Util.toByteArray("A0 00 00 00 04 10 10")); // Mastercard
// CLS INS P1 P2 Le
var selectADFCommand = Util.toByteArray("00 A4 04 00 07 00 00 00 00 00 00 00");
ResponseAPDU answer;
byte[] foundAID = null;
for (var aid : candidateAIDs) {
// copy AID to command array
for (int i=0; i<7; i++) {
selectADFCommand[5+i] = aid[i];
}
answer = channel.transmit(new CommandAPDU(selectADFCommand));
if (answer.getSW() == 0x9000) {
foundAID = aid;
break;
}
}
return foundAID;
}
Выбор ADF и разбор его FCI¶
Теперь, когда AID платёжного приложения известен, можно начинать с ним работу. Так как мы пишем не настоящий банковский терминал, ограничимся только вытаскиванием свободно лежащих данных.
Выбор DF приложения делается так же, как и выбор DF PSE — через команду SELECT. Продолжим с полученным на прошлом этапе AID в переменной aid
:
byte[] selectCommand = Util.toByteArray("00 A4 04 00 07 00 00 00 00 00 00 00");
for (int i=0; i<7; i++) {
selectCommand[5+i] = aid[i];
}
answer = channel.transmit(new CommandAPDU(selectCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardOperationFailedException("No EMV app found.");
}
Эта команда также возвращает FCI. В моём случае он выглядит так:
TAG: 6F (CONSTRUCTED)
TAG: 84 (PRIMITIVE)
VALUE: A0 00 00 00 03 10 10
TAG: A5 (CONSTRUCTED)
TAG: 50 (PRIMITIVE)
VALUE: 56 49 53 41
TAG: 5F 2D (PRIMITIVE)
VALUE: 72 75 65 6E
TAG: 87 (PRIMITIVE)
VALUE: 01
TAG: BF 0C (CONSTRUCTED)
TAG: 9F 4D (PRIMITIVE)
VALUE: 0B 0A
Полностью этот шаблон описан в стандарте 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, опциональное):
- тег
50
— Application name (M) — здесь хранится название платёжной системы, в данном случае VISA; - тег
5F 2D
— Language Preference (O) — здесь хранится предпочитаемый язык в виде максимум четырёх двухбуквенных кодов из ISO 639, в данном случае72 75 65 6E
→ruen
; - тег
87
— Application Priority Indicator (O) — индикатор приоритета приложения, описан в таблице там же Table 48: Format of Application Priority Indicator; - тег
BF 0C
— FCI Issuer Discretionary Data (O) — в это поле включается вся остальная информация от производителя карты, банка, производителя приложения, карточной операционной системы и так далее, допустимые теги и их семантика определены в EMV_v4.3, книга 3, раздел Annex B Rules for BER-TLV Data Objects.
FCI для другой карты выглядит так:
TAG: 6F (CONSTRUCTED)
TAG: 84 (PRIMITIVE)
VALUE: A0 00 00 00 03 10 10
TAG: A5 (CONSTRUCTED)
TAG: 50 (PRIMITIVE)
VALUE: 56 69 73 61 20 43 6C 61 73 73 69 63
TAG: 9F 38 (PRIMITIVE)
VALUE: 9F 1A 02
Здесь мы видим поле с тегом 9F 38
, оно определяется так:
- тег
9F 38
— PDOL (O) — Processing Options Data Object List, содержит специальные данные, необходимые для начала финансовой транзакции. Это поле очень важное, в нём приложение указывает, какие дополнительные данные ему нужны для инициализации финансовой транзакции, я ниже покажу, как это значение использовать.
Распечатаем часть данных из FCI:
var data = answer.getData();
byte[] pdolData = null;
try {
var fciTlv = BerTlv.parseBytes(data);
System.out.println(fciTlv);
var piTlv = fciTlv.getPart("A5");
var labelTlv = piTlv.getPart("50");
System.out.printf("Application name: %s%n", Util.bytesToString(labelTlv.getValue()));
var langTlv = piTlv.getPart("5F 2D");
if (langTlv != null) {
System.out.printf("Language preference: %s%n", Util.bytesToString(langTlv.getValue()));
}
var pdolTlv = piTlv.getPart("9F 38");
if (pdolTlv != null) {
pdolData = pdolTlv.getValue();
}
} catch (BerTlv.ConstraintException e) {
card.disconnect(false);
throw new Util.CardOperationFailedException("Failed to parse SELECT response");
} catch (BerTlv.ParsingException e) {
card.disconnect(false);
throw new Util.CardOperationFailedException("Failed to parse SELECT response");
}
Старт финансовой транзакции через 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}
один или два байта, размер {Li}
— один байт. PDOL
определяет, какую дополнительную информацию от нас ждёт карта, чтобы корректно выполнить команду. Например, тег 9F 35
задаёт тип терминала (Terminal Type), а тег 9F 1A
— двухсимвольный код страны. Чтобы стало понятнее, вот пример:
9F 1A 02 9а 35 01 9F 15 02
Разобъём на компоненты:
{T1} {L1} {T2} {L2} {T3} {L3}
{9F 1A} {02} {9A 35} {01} {9F 15} {02}
DOL формируется только из байтов данных (без тегов), идущих подряд в том же порядке, что элементы в PDOL. Перед данными указывается тег 83
и байт с общей длиной, равной {L1} + {L2} + ... + {Ln}
. Если программа не знает тег или не хочет его заполнять, то может передать заполненный нулями массив. Мы именно так и поступим для простоты.
Если поля PDOL в FCI ADF не было, то передаём Lc=02
и 83 00
, то есть C-APDU: 80 A8 00 00 02 83 00 00
. Вы не можете передать 83 00
, если PDOL был указан.
Подготовим DOL (создадим заполненный нулями массив нужной длины, я использую собственный метод concatArrays
из класса Util, который возвращает новый массив, полученный конкатенацией двух переданных):
// prepare dolData
var dolData = Util.toByteArray("83 00");
if (pdolData != null) {
// parse pdol data and extract total fields length
// ignore tags
boolean lengthByte = false;
int totalLength = 0;
for (byte b : pdolData) {
if (lengthByte) {
int x = b;
if (x < 0) {
x += 256;
}
totalLength += x;
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;
}
}
var t = new byte[totalLength];
dolData[1] = (byte)totalLength; // remember, dolData = "83 00"
dolData = Util.concatArrays(dolData, t);
}
И вызовем инструкцию GET PROCESSING OPTIONS:
// Send command "GET PROCESSING OPTIONS"
var gpoCommand = Util.toByteArray("80 A8 00 00 00");
gpoCommand[4] = (byte)dolData.length;
gpoCommand = Util.concatArrays(gpoCommand, dolData);
gpoCommand = Util.concatArrays(gpoCommand, Util.toByteArray("00"));
System.out.printf("APDU: %s%n", Util.hexify(gpoCommand));
answer = channel.transmit(new CommandAPDU(gpoCommand));
if (answer.getSW() != 0x9000) {
card.disconnect(false);
throw new Util.CardOperationFailedException(String.format("GET PROCESSING OPTIONS failed: %04X%n", answer.getSW()));
}
❈ ❈ ❈
Результат выполнения команды GET PROCESSING OPTIONS — это BER-TLV объект, в котором закодированы два значения: AIP и AFL. Если тег этого объекта 77
, то они представлены как вложенные BER-TLV объекты. Если тег 80
, то в виде байтовой строки.
- 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
Тег 80
говорит нам, что это примитивные данные, значит их структура следующая:
tag │ len │ AIP │ AFL
────┼─────┼───────┼───────────────────────────────────────
80 │ 0E │ 7C 00 │ 08 01 01 00 10 01 05 00 18 01 02 01
Вот другой пример результата, здесь уже закодированные в BER-TLV данные (тег 77):
77 0E 82 02 39 00 94 08 28 01 03 01 30 01 02 00
В раскодированном виде они выглядят так:
TAG: 77(CONSTRUCTED)
TAG: 82(PRIMITIVE)
VALUE: 39 00
TAG: 94(PRIMITIVE)
VALUE: 28 01 03 01 30 01 02 00
Здесь в поле с тегом 82
хранится AIP, в поле с тегом 94
— AFL.
В AIP данные закодированы битами, их структура описана в Таблице 37 книги 3 EMV_v4.3. В коде ниже я покажу, как эти данные считывать.
Чтение данных платёжного приложения¶
Теперь мы готовы прочитать данные приложения, они хранятся в файлах, на которые ссылаются данные из 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
. Аналогично разбирается вторая четвёрка.
❈ ❈ ❈
Итак, прочитаем AIP и AFL из результата GET PROCESSING OPTIONS в переменные aipData и aflData:
data = answer.getData();
byte[] aipData = null;
byte[] aflData = null;
try {
var gpoTlv = BerTlv.parseBytes(data);
if (gpoTlv.tagEquals("77")) {
aipData = gpoTlv.getPart("82").getValue();
aflData = gpoTlv.getPart("94").getValue();
} else if (gpoTlv.tagEquals("80")) {
var gpoData = gpoTlv.getValue();
aipData = Util.copyArray(gpoData, 0, 2);
aflData = Util.copyArray(gpoData, 2, gpoData.length-2);
} else {
throw new Util.CardOperationFailedException("Unknown response from GET PROCESSING OPTIONS command");
}
} catch (BerTlv.ConstraintException e) {
throw new Util.CardOperationFailedException("Failed to decode response from GET PROCESSING OPTIONS command");
} catch (BerTlv.ParsingException e) {
throw new Util.CardOperationFailedException("Failed to parse response from GET PROCESSING OPTIONS command");
}
Сначала раскодируем информацию из AIP (напомню, она определена в EMV_v4.3, книга 3, Таблица 37), просто сравниваем отдельные биты:
System.out.println("> Application Interchange Profile");
System.out.printf(" Static Data Authentication supported: %s%n", (aipData[0] & 0x40)==0 ? "no" : "yes");
System.out.printf(" Dynamic Data Authentication supported: %s%n", (aipData[0] & 0x20)==0 ? "no" : "yes");
System.out.printf(" Cardholder verification is supported: %s%n", (aipData[0] & 0x10)==0 ? "no" : "yes");
System.out.printf(" Terminal risk management is to be performed: %s%n", (aipData[0] & 0x8)==0 ? "no" : "yes");
System.out.printf(" Issuer authentication is supported: %s%n", (aipData[0] & 0x4)==0 ? "no" : "yes");
System.out.printf(" CDA supported: %s%n", (aipData[0] & 0x1)==0 ? "no" : "yes");
Дальше разберём AFL на компоненты, распарсим их, прочитаем все записи из указанных файлов, получим на выходе плоский список BER-TLV объектов в переменной readObjects
:
// now read AFL points to
var readObjects = new ArrayList<BerTlv>(10);
int aflPartsCount = aflData.length / 4;
for (int i=0; i<aflPartsCount; i++) {
int startByte = i*4;
var sfi = (byte)(aflData[startByte] >> 3);
int firstSfiRec = aflData[startByte + 1];
int lastSfiRec = aflData[startByte + 2];
int offlineAuthRecNumber = aflData[startByte + 3]; // we don't use it
// CLA INS P1 P2 Le
var readRecordCommand = Util.toByteArray("00 B2 00 00 00");
for (int j=firstSfiRec; j<=lastSfiRec; j++) {
// set Le=0
readRecordCommand[4] = 0;
// set P1
readRecordCommand[2] = (byte)j;
byte p2 = (byte)((sfi << 3) | 4);
readRecordCommand[3] = p2;
answer = channel.transmit(new CommandAPDU(readRecordCommand));
if (answer.getSW1() == 0x6C) {
// set new Le and repeat
readRecordCommand[4] = (byte)answer.getSW2();
answer = channel.transmit(new CommandAPDU(readRecordCommand));
}
if (answer.getSW() != 0x9000) {
// real terminal must terminate transaction if any read fails
System.out.printf("Failed to read record %d from SFI=%d", j, sfi);
continue;
}
var recordData = answer.getData();
try {
BerTlv recordTlv = BerTlv.parseBytes(recordData);
if (!recordTlv.tagEquals("70")) {
continue;
}
try {
for (BerTlv p : recordTlv.getParts()) {
readObjects.add(p);
}
} catch (BerTlv.ConstraintException e) {
System.out.println("BER-TLV error");
}
} catch (BerTlv.ParsingException e) {
System.out.printf("Failed to parse data: %s%n%s%n", e, Util.hexify(recordData));
}
}
}
Приведём значения этих объектов в читаемую форму, для этого мы должны для каждого объекта проинтерпретировать его данные в зависимости от значения тега, а также подставить название вместо кода тега. Я написал в модуле Util.java функцию Util.mapDataObjects()
, которая возвращает HashMap с интерпретированными (по возможности) значениями для некоторых BER-TLV тегов из EMV. При этом главный код выглядит так:
System.out.println("> AFL Data");
var mappedValues = Util.mapDataObjects(readObjects);
for (var b : readObjects) {
String tagString = Util.hexify(b.getTag());
System.out.printf(" %s%n", mappedValues.get(tagString));
}
Результат работы программы на одной из моих карты (я заменил потенциально приватные данные):
Using terminal PC/SC terminal ACS ACR38U-CCID
Found PSE, extract AID
Found payment system: A0 00 00 00 04 10 10
Application name: MasterCard
Language preference: ruenfrde
> 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: yes
> AFL Data
Application Effective Date: 2011-09-01
Application Expiration Date: 2016-03-31
Application Usage Control: FF 00
Application Primary Account Number (PAN): XX XX XX XX XX XX XX XX
Application Primary Account Number (PAN) Sequence Number: 01
Cardholder Verification Method (CVM) List: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
Issuer Action Code - Default: XX XX XX XX XX
Issuer Action Code - Denial: 00 00 80 00 00
Issuer Action Code - Online: XX XX XX XX XX
Issuer Country Code: 06 43
Static Data Authentication Tag List: 82
Card Risk Management Data Object List 1 (CDOL1): XX XX XX XX XX XX XX XX XX XX XX XX XX XX
Card Risk Management Data Object List 2 (CDOL2): XX XX XX XX XX XX XX XX XX XX XX
Track 2 Equivalent Data: XX XX XX XX XX XX XX XX XX XX XX
Cardholder Name: STOLYAROV/SERGEY
Application Version Number: 00 02
Application Currency Code: 06 43
Certification Authority Public Key Index: 05
Issuer Public Key Exponent: 03
Issuer Public Key Remainder: XX XX XX XX XX
Issuer Public Key Certificate: XX XX XX XX XX
Dynamic Data Authentication Data Object List (DDOL): 9F 37 04
ICC Public Key Exponent: 03
ICC Public Key Certificate: XX XX XX XX XX
❈ ❈ ❈
И в завершение примера диаграмма, показывающая структуру файлов, как её видит терминал:
- DDF — Directory Definition File в терминологии EMV, является DF по ISO/IEC 7816
- AEF — Application Elementary File в терминологии EMV, является EF по ISO/IEC 7816
- PSE — Payment System Environment, опциональный DDF, содержащий EF со списком установленных платёжных приложений
- PSD — Payment System Directory — это EF, содержащий имена платёжных приложений
- ADF — Application 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 включительно. В каждом байте кодируется одно или несколько именованных значений, а сами байты тоже имеют имена-обозначения:
В начале идёт обязательный байт формата — T0, в нём закодированы два значения: индикатор Y1 и число K следующим образом:
Интерфейсные байты TAi, TBi, TCi, TDi кодируют протоколы, которые предлагает карта, и параметры этих протоколов. Эти байты могут быть глобальными (global), тогда они касаются всех протоколов или каких-то параметров карты в целом; или специфичными для какого-то конкретного протокола.
TAi, TBi, TCi содержат данные, а TDi также определяет будут ли дальше в массиве следующие интерфейсные байты. Каждый байт TDi кодирует индикатор Yi+1 и тип T:
Тип 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, раздел 8.1.1 Historical bytes.
Первый байт — это индикатор категории (category indicator), он может принимать следующие значения:
00
- последующие данные состоят из закодированных в COMPACT-TLV объектов плюс в конце три обязательных байта индикатора состояния (status indicator, см. ISO/IEC 7816-4, раздел 8.1.1.3 Status indicator);
10
- следующий байт является ссылкой на данные DIR (честно говоря, не имею ни малейшего представления, что это значит, оно имеет смысл для синхронных карт памяти, но там другая структура ATR, поэтому просто игнорирую);
80
- последующие данные состоят из закодированных в COMPACT-TLV объектов, среди которых может находиться индикатор состояния (BER-TLV тег
48
, компактный тег81
,82
или83
); 81 - 8F
- зарезервировано для будущего использования;
XX
- все остальные значения означают проприетарный формат байтов предыстории, который не покрывается стандартом ISO/IEC 7816.
Упомянутый COMPACT-TLV — это способ кодирования межотраслевых BER-TLV объектов с тегом 4X
:
- если тег объекта —
4X
; - если длина блока данных —
0Y
; - то значение
XY
называется компактным тегом, при этом сам COMPACT-TLV объект состоит из тегаXY
и последующих Y байтов.
Обратное декодирование происходит аналогично.
Теги 4X
— это теги класса приложения (то есть их семантика зафиксирована) и они определены в разных частях стандарта ISO/IEC 7816. Вот они все (RFU — reserved for future use, значение тега не определено на данный момент):
40
— RFU41
— Country code and national data (ISO/IEC 7816-4, 8.1.1.2.1 Country or issuer indicator)42
— Issuer identification number (ISO/IEC 7816-4, 8.1.1.2.1 Country or issuer indicator)43
— Card service data (ISO/IEC 7816-4, 8.1.1.2.3 Card service data)44
— Initial access data (ISO/IEC 7816-4, 8.1.1.2.4 Initial access data)45
— Card issuer's data (ISO/IEC 7816-4, 8.1.1.2.5 Card issuer's data)46
— Pre-issuing data (ISO/IEC 7816-4, 8.1.1.2.6 Pre-issuing data)47
— Card capabilities (ISO/IEC 7816-4, 8.1.1.2.7 Card capabilities)48
— Status information (ISO/IEC 7816-4, 8.1.1.3 Status indicator)49
— RFU4A
— RFU4B
— RFU4C
— RFU4D
— Extended header list (ISO/IEC 7816-4, 8.5.1 Indirect references to data elements)4E
— RFU4F
— Application identifier (ISO/IEC 7816-4, 8.1.1.2.2 Application identifier)
Подробную семантику этих объектов вы можете найти либо в указанных частях стандарта, либо в примере ниже.
❈ ❈ ❈
Байт TCK является опциональным и служит для проверки целостности ATR, он содержит XOR всех байтов, начиная с T0 и заканчивая байтом перед TCK.
В PC/SC первым байтом ATR является не T0, а TS, он определён в ISO/IEC 7816-3 и задаёт электрические/сигнальные параметры коммуникации. Несмотря на то, что в стандарте TS не включён в ATR, PC/SC всё равно его возвращает. Это нужно учитывать при разборе.
example-10: Разбор ATR¶
Для этого примера можно использовать оба типа терминалов: для бесконтактных и контактных чиповых карт.
Исходный код: https://github.com/sigsergv/pcsc-tutorial-java/blob/master/example-10/Example.java
Цель этого примера: полностью и максимально подробно разобрать ATR, для этого мы напишем отдельный метод для разбора байтового массива ATR и печати разобранных данных.
Определим новое приватное исключение для внутреннего использования и новый приватный класс для представления группы интерфейсных байтов:
private static class ATRParsingException extends Exception {
public ATRParsingException(String message) {
super(message);
}
}
private static class InterfaceBytes {
public final Byte TA;
public final Byte TB;
public final Byte TC;
public final Byte TD;
public final int T;
public InterfaceBytes(Byte TA, Byte TB, Byte TC, Byte TD, int T) {
this.TA = TA;
this.TB = TB;
this.TC = TC;
this.TD = TD;
this.T = T;
}
};
В классе InterfaceBytes
null в любом из полей TA, TB, TC и TD сигнализирует об отсутствии данного байта.
Весь код разбора поместим вот в такой try..catch
блок:
try {
...............
...............
...............
} catch (java.lang.ArrayIndexOutOfBoundsException e) {
System.out.println("Failed to parse ATR: internal structure is broken");
} catch (ATRParsingException e) {
System.out.printf("Failed to parse ATR: %s%n", e.toString());
}
Исключение java.lang.ArrayIndexOutOfBoundsException
здесь нужно для контроля, что при разборе не вылезли некорректно за пределы исходного массива.
Несколько базовых проверок:
// first extract data
if (bytes.length < 2) {
throw new ATRParsingException("ATR bytes array is too short");
}
// check TS byte
if (bytes[0] != 0x3B && bytes[0] != 0x3F) {
throw new ATRParsingException("Byte TS is incorrect.");
}
Определяем переменные, где будут лежать разобранные данные:
ArrayList<InterfaceBytes> allInterfaceBytes = new ArrayList<InterfaceBytes>(33);
int historicalBytesLength = 0;
byte[] historicalBytes = new byte[0];
И начинаем с разбора байта формата:
// check format byte T0
byte T0 = bytes[1];
int Y = (T0 >> 4) & 0xF;
historicalBytesLength = T0 & 0xF;
Мы используем одну переменную для хранения текущего Yi, поскольку нет необходимости хранить их все.
Далее читаем все интерфейсные байты с записью их в список allInterfaceBytes
, в конце цикла обновляем переменные T и Y новыми значениями:
// read interface bytes
int p = 2;
int T = 0;
while (true) {
Byte TA = null;
Byte TB = null;
Byte TC = null;
Byte TD = null;
// check is next byte is TAi
if ((Y & 1) != 0) {
TA = bytes[p];
p++;
}
// check is next byte is TBi
if ((Y & 2) != 0) {
TB = bytes[p];
p++;
}
// check is next byte is TCi
if ((Y & 4) != 0) {
TC = bytes[p];
p++;
}
// check is next byte is TDi
if ((Y & 8) != 0) {
TD = bytes[p];
p++;
}
allInterfaceBytes.add(new InterfaceBytes(TA, TB, TC, TD, T));
if (TD == null) {
break;
}
T = TD & 0xF;
Y = (TD >> 4) & 0xF;
}
Скопируем байты предыстории:
// copy historical bytes
historicalBytes = Util.copyArray(bytes, p, historicalBytesLength);
Прочитаем и проверим байт TCK, если он есть.
❈ ❈ ❈
Теперь можно распечатать данные, начнём с интерфейсных байтов:
System.out.printf("ATR: %s%n", Util.hexify(bytes));
System.out.println("Interface bytes:");
p = 1;
for (InterfaceBytes tb : allInterfaceBytes) {
if (tb.TA != null) {
System.out.printf(" TA%d = %02X (T = %d)%n", p, tb.TA, tb.T);
}
if (tb.TB != null) {
System.out.printf(" TB%d = %02X (T = %d)%n", p, tb.TB, tb.T);
}
if (tb.TC != null) {
System.out.printf(" TC%d = %02X (T = %d)%n", p, tb.TC, tb.T);
}
if (tb.TD != null) {
System.out.printf(" TD%d = %02X (T = %d)%n", p, tb.TD, tb.T);
}
p++;
}
С точки зрения внешнего приложения ничего особо интересного в интерфейсных байтах нет, там содержатся только данные для низкоуровневого протокола. А вот байты предыстории я постараюсь разобрать подробнее, так как там встречается полезная информация.
В ISO/IEC 7816-4, в разделе 8.1.1 Historical bytes описываются правила разбора байтов предыстории. У себя в коде я это делаю в методах printHistoricalBytesValue
, getStatusIndicatorBytes
, getCapabilities
. Я там преобразую теги в записях COMPACT-TLV в традиционные теги BER-TLV и разбираю содежимое в соответствии со стандартом.
❈ ❈ ❈
Результат работы на разных картах.
Смарт-карта сотовой связи МТС:
Using terminal PC/SC terminal ACS ACR38U-CCID
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
Data unit size in quartets: 1
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
Заключение¶
На этом я статью хочу завершить и подвести итоги. Главной моей целью был обзор технологий и стандартов для практического использования смарт-карт. Самые главные моменты, которые вы должны были усвоить после прочтения:
- PC/SC как главная коммуникационная библиотека и java-интерфейс к ней в виде пакета java.smartcardio;
- APDU как способ коммуникации с картой из внешнего приложения;
- основные отраслевые стандарты;
- принципы работы с данными на смарт-карте;
- кодирование объектов (SIMPLE-TLV, BER-TLV).
Я планирую писать новые статьи с примерами кода, которые будут предполагать знакомство с данным текстом в качестве базового. Примерные темы новых статей:
- работа с NFC-метками;
- работа с EMV-картами (банковские карты);
- работа с SIM-картами;
- работа с крипто-токенами (OpenPGP, PIV);
- работа с отечественными крипто-токенами (JaCarta, eToken, Aladdin).
Зачем мои комменты удалили :D
Последствия миграции на новую версию, восстановил.
Я провел несколько недель читая и разбирая эту статью и проделывая всё это на андроиде. Это золото!
Хотелось бы добавить, что при бесконтактном NFC поиск AID проходит гораздо проще. У каждой банковской карты ВСЕГДА есть 2PAY.SYS.DDF01 (так же как 1pay но с двойкой). И в нем всегда есть AID или несколько AID.
Чтобы заработало на macOS 12 Monterey, пришлось задать пропертю -Dsun.security.smartcardio.library="/System/Library/Frameworks/PCSC.framework/Versions/Current/PCSC"
Установленная джава: % java -version openjdk version "15.0.2" 2021-01-19 OpenJDK Runtime Environment AdoptOpenJDK (build 15.0.2+7) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 15.0.2+7, mixed mode, sharing)
Сергей, просто нет слов! Браво! Материал, примеры, подача. Вам книги писать надо! Какие только ресурсы не излазил, ничего подобного (для новичка) не найти: все на уровне "Hello, NFC".
Благодарю Вас за этот труд!
Спасибо за отзыв!
Сергей, интересует сотрудничество, но не нашел контакта для связи с вами, можете связаться со мной?