Expertus metuit
Смарт-карты и программирование
2017-03-10 11:54

Это статья о смарт-картах и о том, как писать софт для работы с ними. Никакого опыта в предметной области от вас не требуется, только знание C и C++ для понимания примеров кода, а также базовых структур данных, битов, байтов, указателей, malloc/new/free/delete и так далее. Все примеры ориентированы на unix-окружение, в первую очередь это linux и mac os x. Windows и мобильные операционные системы не рассматриваются.

Для всех примеров кода вам нужен десктопный терминал-ридер для смарт-карт. Для некоторых примеров подойдёт USB-крипто-токен — они работают через тот же интерфейс, что и смарт-карты.

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

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

Первые примеры будут на С — это язык библиотеки pcsc, а затем переключусь на C++11 и собственную библиотеку-обёртку над pcsc.

Примерный план статьи:

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

Стандарты

Я буду очень много ссылаться на стандарты, поэтому для более глубокого понимания вам нужно обязательно иметь их под рукой. К сожалению, стандарты ISO/IEC недоступны для свободного скачивания, однако их можно найти в интернете. Русские переводы соответствующих стандартов, однако, доступны и ссылки на них вы найдёте в тексте статьи, а также в самом конце.

Многие другие стандарты доступны для свободного скачивания. Ссылки на них также в конце статьи.

Перед началом чтения рекомендую ознакомиться со статьй в википедии о смарт-картах — https://ru.wikipedia.org/wiki/Смарт-карта, там описаны все базовые и элементарные вещи, которые я не хочу повторять.

Оборудование и средства разработки

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

ACS ACR122U

❈ ❈ ❈

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

ACS ACR38U

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

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

Вам может пригодиться документация к API этих терминалов (версии на момент написания статьи — 8 апреля 2017 г.):

Дальше по тексту я буду называть считыватели (ридеры) смарт-карт или NFC-карт терминалами.

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

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

❈ ❈ ❈

Базовая операционная система для экспериментов — Mac OS X El Capitan или Debian/Ubuntu linux. Скорее всего, всё будет работать и с другими версиям Mac OS X и другими разновидностями линукса. Язык программирования — C и C++. Компилятор — gcc (linux) или clang-gcc (Mac OS X). Все примеры условно кроссплатформенные и должны без модификаций собираться на linux (debian/ubuntu) и Mac OS X. Для сборки используется GNU make.

Традиционный дисклеймер: в работе я активно использую терминал и shell. Все примеры запуска команд приводятся для shell-сессии вместе с выводом работы.

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

Я рекомендую такой процесс работы с текстом:

  • скачайте репозиторий с гихаба
  • установите все нужные для компиляции программы и библиотеки
    • в debian/ubuntu: sudo apt install libpcsclite-dev gcc pcscd make
    • в Macos нужно установить Command Line Tools for Xcode, инструкции найдёте в интернете
  • читайте текст
  • изучайте примеры кода
  • компилируйте и запускайте программы из соответствующего каталога
  • модифицируйте программы как угодно.

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

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

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

Отладка в Macos

Начиная с версии Yosemite, в Macos появился штатный инструмент для мониторинга работы ридеров смарт-/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-сессии и оставить там работать.

Отладка в linux

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

% 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 
...

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

В Macos для разработчика фактически доступен только один интерфейс для работы со смарт-картами и NFC-картами, он реализован в виде нескольких системных сервисов, а его интерфейсная часть — фреймворк PCSC. PCSC расшифровывается как Personal Computer/Smart Card и является портом (с практически идентичным API) с Windows. Изначально в Macos включалась библиотека pcsc-lite и демон pcscd, однако позднее внутренности были полностью переделаны, демон pcscd исключён, но API остался прежним.

PC/SC является спецификацией, предложенной PC/SC Workgroup, именно работе с ней (и только с ней!) посвящена эта статья.

К сожалению, мне не удалось найти официальной документации на PCSC для Macos, но можно пользоваться документаций проекта pcsc-lite, только имейте в виду, что некоторые описанные там вещи в Macos не поддерживаются.

Также в обновлённых заголовочных файлах в Macos вместо windows-типов используются стандартные из gcc, например, вместо LONG — int32_t. В моём коде я буду использовать именно windows-типы для всех pcsc-функций, как это принято в оригинальной библиотеке pcsc-lite. Эти типы подключаются в заголовочном файле wintypes.h.

Ссылка на документацию: https://pcsclite.alioth.debian.org/api/

Этот текст посвящён исключительно PC/SC, другие низкоуровневые команды, протоколы и спецификации не рассматриваются. Работа с NFC также происходит исключительно через PC/SC.

example-01: инициализация библиотеки

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

Начнём с импорта заголовочных файлов PCSC. В Macos и linux они отличаются, поэтому я использую директивы условной компиляции:

#ifdef __APPLE__
#include <PCSC/pcsclite.h>
#include <PCSC/winscard.h>
#include <PCSC/wintypes.h>
#else
#include <pcsclite.h>
#include <winscard.h>
#include <wintypes.h>
#endif

Первая задача:

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

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

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

LONG result;
SCARDCONTEXT sc_context;

result = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &sc_context);
if (result != SCARD_S_SUCCESS) {
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}
printf("Connection to PC/SC established\n");

Дальше нужно получить список терминалов, это делается через функцию SCardListReaders(). Сначала мы её вызываем с третьим аргументов выставленным в 0, это говорит библиотеке, что нужно вычислить, сколько необходимо байтов для хранения этого списка, это значение сохраняется в переменной readers_size. Ранее полученный «контекст» sc_context передаём первым аргументом.

DWORD readers_size;

result = SCardListReaders(sc_context, NULL, 0, &readers_size);
if (result != SCARD_S_SUCCESS) {
    SCardReleaseContext(sc_context);
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}

В случае ошибки мы освобождаем контекст вызовом функции SCardReleaseContext(), это нужно делать обязательно!

Дальше нужно получить и прочитать список. Он представляет собой набор строк, разделённых нулевым байтом. Это популярный в windows формат представления строковых списков, который обычно называется double-null-terminated string . По сути это идущие подряд одна за другой NULL-terminated строки, плюс ещё один NULL в конце блока. Список из трёх строк one, two, three в таком представлении будет выглядеть в памяти списком байтов: one\0two\0three\0\0.

LPSTR readers;
readers = calloc(1, readers_size);

result = SCardListReaders(sc_context, NULL, readers, &readers_size);
if (result != SCARD_S_SUCCESS) {
    SCardReleaseContext(sc_context);
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}

printf("Found readers:\n");
int n = 0;
for (int i = 0; i < readers_size - 1; ++i) {
    ++n;
    printf("  Reader #%d: %s\n", n, &readers[i]);
    while (readers[++i] != 0);
}
printf("total: %i\n", n);

В этом фрагменте мы сначала выделяем память под список строк функцией calloc() размером readers_size и затем передаём эту переменную в функцию SCardListReaders(), где она заполняется значениями.

Всё, теперь можно отключиться от библиотеки:

result = SCardReleaseContext(sc_context);
if (result != SCARD_S_SUCCESS) {
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}
printf("Connection to PC/SC closed\n");

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

% make && ./example-01
cd ../util && make
make[1]: `libutil.a' is up to date.
cc -Wall -c -o example-01.o example-01.c
cc -o example-01 example-01.o   ../util/libutil.a -framework PCSC
Connection to PC/SC established
Found readers:
  Reader #1: ACS ACR122U
  Reader #2: ACS ACR38U-CCID
total: 2
Connection to PC/SC closed

❈ ❈ ❈

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

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

  • Для доступа к NFC и смарт-карте мы пользуемся одной и той же библиотекой для смарт-карт — PC/SC.
  • NFC-терминал видится библиотекой как считыватель смарт-карт.
  • Перед началом использования терминала нужно в программе инициализировать библиотеку.
  • В примере я «забыл» освободить ранее выделенную вызовом calloc() память, не повторяйте моих ошибок.
  • Непосредственно с терминалом мы пока не работали, а только с общей библиотекой.
  • В библиотеке и у меня используются «виндовые» названия типов, я так делаю специально, чтобы максимально соответствовать документации pcsc-lite.

example-02: Подключение к терминалу

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

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

// выделение памяти под список терминалов
LPSTR readers;
readers = calloc(1, readers_size);

result = SCardListReaders(sc_context, NULL, readers, &readers_size);
if (result != SCARD_S_SUCCESS) {
    SCardReleaseContext(sc_context);
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}

// возьмём первый терминал, 256 байтов должно хватить
char reader_name[256] = {0};

if (readers_size > 1) {
    strncpy(reader_name, readers, 255);
} else {
    printf("No readers found!\n");
    return 2;
}

// освобождаем память
free(readers);

Здесь мы сохраняем имя терминала в переменной reader_name и освобождаем ранее выделенную память для переменной readers.

❈ ❈ ❈

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

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

Так как подключение требует уже вставленной карты, нам необходимо сначала дождаться её появления в терминале. Для этого используется функция SCardGetStatusChange(), она блокирует исполнение кода, пока состояние указанного терминала (или терминалов) не изменится. Нужные терминалы указываются в структуре типа SCARD_READERSTATE, которая передаётся в функцию, в нашем случае он один.

SCARD_READERSTATE sc_reader_states[1];
sc_reader_states[0].szReader = reader_name;
sc_reader_states[0].dwCurrentState = SCARD_STATE_EMPTY;

result = SCardGetStatusChange(sc_context, INFINITE, sc_reader_states, 1);
if (result != SCARD_S_SUCCESS) {
    SCardReleaseContext(sc_context);
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}

Константа SCARD_STATE_EMPTY в поле dwCurrentState означает, что функция ждёт, когда будет вставлена карта. Если в терминале карта уже есть на момент вызова, функция успешно завершится сразу.

Успешное завершение (то есть с кодом SCARD_S_SUCCESS) означает, что можно подключаться. Хэндл соединения сохраняется в переменной reader, он должен использоваться для всех дальнейших операций с картой в терминале.

SCARDHANDLE reader;
DWORD active_protocol;

result = SCardConnect(sc_context, reader_name, 
    SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
    &reader, &active_protocol);
if (result != SCARD_S_SUCCESS) {
    SCardReleaseContext(sc_context);
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}

if (active_protocol == SCARD_PROTOCOL_T0) {
    printf("  Card connected, selected protocol: T=0.\n");
}
if (active_protocol == SCARD_PROTOCOL_T1) {
    printf("  Card connected, selected protocol: T=1.\n");
}

При вызове SCardConnect() мы указываем режим совместного использования (SCARD_SHARE_SHARED), предпочитаемый протокол связи (в нашем случае SCARD_PROTOCOL_T0 или SCARD_PROTOCOL_T1), передаём адрес переменной reader, в которую будет сохранён хэндл для дальнейших операций с картой и адрес переменной active_protocol, куда сохраняется выбранный терминалом протокол связи.

Для связи терминала и смарт-карты (или NFC-карты) используется один из транспортных протоколов: T=1 или T=0. T=0 исторически первый, он байто-ориентированный. T=1 — более поздний, он блок-ориентированный. Оба протокола полу-дуплексные асинхронные. Вместо T=0 можно писать T0 и наоборот. Для T1/T=1 аналогично. На уровне приложения нас не особо интересует, какой именно протокол был выбран для установки соединения, однако эту информацию нужно сохранить — она понадобится позднее для отправки команд.

❈ ❈ ❈

Запросим у терминала его статус, для этого используется функция SCardStatus(). Обратите внимание, что мы не передаём sc_context, а только хэндл терминала.

char reader_friendly_name[MAX_READERNAME];
DWORD reader_friendly_name_size = MAX_READERNAME;
DWORD state;
DWORD protocol;
DWORD atr_size = MAX_ATR_SIZE;
BYTE atr[MAX_ATR_SIZE];

result = SCardStatus(reader, reader_friendly_name, &reader_friendly_name_size, 
    &state, &protocol, atr, &atr_size);
if (result != SCARD_S_SUCCESS) {
    SCardDisconnect(reader, SCARD_RESET_CARD);
    SCardReleaseContext(sc_context);
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}

printf("  Card ATR: ");
print_bytes(atr, atr_size);

Для печати массива байтов я использую функцию print_bytes из библиотеки util, она лежит в этом же репозитории неподалёку. Константа MAX_READERNAME определена в заголовочных файлах библиотеки pcsc.

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

Структура ATR определена в разделе 8.2 стандарта ISO/IEC 7816-3.

❈ ❈ ❈

Отключаемся от терминала функцией SCardDisconnect(), константа SCARD_RESET_CARD указывает терминалу, что нужно «сбросить» карту, но не выключать и не извлекать её:

// отключение от терминала
result = SCardDisconnect(reader, SCARD_RESET_CARD);
if (result != SCARD_S_SUCCESS) {
    SCardReleaseContext(sc_context);
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}

После этой команды хэндл в переменной reader больше нельзя использовать для операций с терминалом.

❈ ❈ ❈

Программа готова, вот результат её компиляции и выполнения (в дебиане, на чистой «фабричной» NFC-карте и терминале ACR122U):

% make && ./example-02
cd ../util && make
make[1]: `libutil.a' is up to date.
cc -Wall -c -o example-02.o example-02.c
cc -o example-02 example-02.o   ../util/libutil.a -framework PCSC
Connection to PC/SC established
Use reader 'ACS ACR122U'
  Card connected, selected protocol: T1.
  Card ATR: 3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 01 00 00 00 00 6A 
Connection to PC/SC closed

А вот результат выполнения на терминале ACR38U и чистой «фабричной» карте с контактной площадкой:

% ./example-02
Connection to PC/SC established
Use reader 'ACS ACR38U-CCID'
  Card connected, selected protocol: T0.
  Card ATR: 3B 04 A2 13 10 91
Connection to PC/SC closed

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

  • Нужно в явном виде дожидаться готовности ридера и появления в нём карты.
  • Перед началом работы с терминалом нужно к нему явным образом подключиться функцией SCardConnect().
  • Полученный хэндл соединения нужно использовать во всех операциях с терминалом.
  • После окончания работы с картой или терминалом нужно явно отключиться функцией SCardDisconnect(), если этого не сделать, но ридер может «зависнуть» и потребуется его физическое переподключение.
  • Карта при подключении к терминалу обязана сразу же передать свой ATR.
  • ATR не является идентификатором карты и не содержит идентификатор карты. Все карты одного типа могут иметь одинаковый ATR.
  • В интернете есть вебсервис, которым можно парсить ATR: http://smartcard-atr.appspot.com/
  • У меня в репозитории есть собственная реализация парсера ATR, об этом я напишу ниже.
  • Поэкспериментируйте с ATR разных карт или NFC-меток.
  • Вы можете вместо карты поднести к терминалу, например, смартфон с включённым NFC и терминал его распознает.
  • У бесконтактной карты (NFC) нет 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-сканер.

❈ ❈ ❈

В стандартах и спецификациях приняты определённые соглашения относительно записи байтов и байтовых массивов. Размер байта — 8 бит. Нотация для байта следующая: пара hex-символов в верхнем регистре, например 02 или F1. В некоторых документах встречается такая запись: 02h или F1h.

Байтовые массивы записываются в виде списка байтов, разделённых пробелами: 00 0A 00 12 AC. Это именно списки байтов в том виде, в каком они хранятся в памяти или передаются по каналу связи. Указанный выше список транслируется в инициализатор C-массива следующим образом: {0x00, 0x0A, 0x00, 0x12, 0xAC}.

Байт 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  │
└────┴────┴────┴────┴────┴────┴────┴────┘

Для обозначения чисел используется C-нотация, например, 0x12A9 или 0x35. Иногд ещё указывается, в каком виде кодируется число — little endian / big endian. Однако в этой предметной области принят по умолчанию вариант big endian.

В статье я буду придерживаться этих соглашений.

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

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

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

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

Архитектура 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 — это чиповая карта с контактной площадкой, в этом разделе рассматриваются только они).

IFD and ICC states

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

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

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

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

Схема работы 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-картой ничем не отличается от работы с картой с контактной площадкой.

Пара команда-ответ состоит из двух идущих подряд сообщений: APDU команды и APDU ответа. Каждый APDU представляет собой последовательность байтов со следующей структурой:

Command-Response

Из этой таблицы сложно сразу понять структуру. К счастью, всего существует четыре структурно отличающихся варианта 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  │ используется для бесконтактных карт

❈ ❈ ❈

Командный байт INS определяет собственно команду (функцию). Полный список стандартных значений можно найти в разделе 5.1.2 стандарта ISO/IEC 7816-4. Например, стандарты для SIM-карт описывают свои команды для взаимодействия с картой.

❈ ❈ ❈

Следующие два байта 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. Длина ответа должна быть как минимум два байта. В двух последних содержится статус выполнения команды. Если команда не возвращает ничего, 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). Она определена только для бесконтактных карт/терминалов и в зависимости от значения P1 возвращает либо UID, либо байты предыстории (historical bytes) из ATS (ATS — Answer-to-Select — некий аналог ATR для PICC, участвующий в процессе инициализации бесконтактной карты в PCD).

Вот байты команды:

FF CA 00 00 00

Поле CLA здесь — FF, это значение является зарезервированным и в данном случае сигнализирует об использовании бесконтактного терминала (NFC).

Поле INS — CA, это код команды Get Data.

Параметры команды:

  • P1=00, P2=00 — для получения UID
  • P1=01, P2=00 — для получения байтов предыстории из ATS (пока не думайте, что такое байты предыстории, просто некие данные в ATR)

Поле Lc отсутствует и, значит, отсутствует следующий за ним блок данных команды. То есть Nc=0.

В поле Le стоит значение 00, то есть мы ожидаем все данные сразу.

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

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

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

Этот пример включает код из предыдущих по инициализации терминала. Единственное отличие — для подключения к терминалу мы требуем протокол T=1, без альтернативы T=0:

result = SCardConnect(sc_context, reader_name, 
    SCARD_SHARE_SHARED, SCARD_PROTOCOL_T1,
    &reader, &active_protocol);

❈ ❈ ❈

Для отправки команды используется функция SCardTransmit(). Первым аргументом традиционно стоит хэндл терминала. Вторым идёт специальная структура с информацией об используемом протоколе, мы используем предопределённую константу SCARD_PCI_T1 для обозначения протокола T=CL (который на самом деле T=1 для бесконтактных карт). В данном случае PCI расшифровывается как Protocol Control Information.

В переменной send_buffer мы передаём байты APDU команды, а следующим аргументом указываем размер этого буфера.

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

// APDU для получения UID
BYTE send_buffer[] = {0xFF, 0xCA, 0x00, 0x00, 0x00};
BYTE recv_buffer[0x20];
DWORD recv_length = sizeof(recv_buffer);

result = SCardTransmit(reader, SCARD_PCI_T1, 
    send_buffer, sizeof(send_buffer), NULL,
    recv_buffer, &recv_length);
if (result != SCARD_S_SUCCESS) {
    SCardDisconnect(reader, SCARD_RESET_CARD);
    SCardReleaseContext(sc_context);
    printf("%s\n", pcsc_stringify_error(result));
    return 1;
}

В последних двух байтах ответа в буфере recv_buffer находятся байты состояния SW1 и SW2. В случае успешного завершения их значения должны быть такими: SW1=90, SW2=00.

Дальше мы печатаем весь ответ целиком, проверяем статус выполнения команды (переменная SW) и печатаем UID карты, если всё прошло успешно.

printf("Response APDU: ");
print_bytes(recv_buffer, recv_length);

BYTE SW1 = recv_buffer[recv_length-2];
BYTE SW2 = recv_buffer[recv_length-1];
int SW = SW1*256 + SW2;

if (SW != 0x9000) {
    printf("Failed to fetch UID! SW1=%02X, SW2=%02X\n", SW1, SW2);
} else {
    printf("Success!\n");
    printf("Card UID: ");
    print_bytes(recv_buffer, recv_length - 2);
}

UID бесконтактной карты определяется в разделе 6.4.4 стандарта ISO/IEC 14443-3 и может быть длиной 4, 7 или 10 байтов. UID в общем случае НЕ является постоянным уникальным идентификатором карты — стандарт допускает его динамическую случайную генерацию (картой). Основная цель UID — идентификация карты в цикле антиколлизии. Считается, что вероятность совпадения UID нескольких карт у одного терминала исчезающе мала.

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

❈ ❈ ❈

Теория: данные на смарт-карте

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

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

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

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

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

ICC architecture

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

❈ ❈ ❈

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

ICC architecture

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

❈ ❈ ❈

Самыми распространёнными бесконтактными картами памяти являются Mifare Classic. Они выпускаются как в виде традиционных пластиковых карт, так и виде брелков, наклеек и т.п. У Mifare собственный проприетарный протокол общения и своя собственная схема работы с данными.

Работа с картами памяти Mifare Classic

Карты Mifare Classic чрезвычайно популярны, поэтому я расскажу подробно, как с ними работать. Подробные спецификации можно скачать с официального сайта: http://cache.nxp.com/documents/data_sheet/MF1S50YYX_V1.pdf. У Mifare Classic свой собственный протокол и не все терминалы его поддерживают. ACR122U такие карты понимает.

В карте Mifare Classic 1K один килобайт в 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. Теоретически он защищён от перезаписи, однако существуют «хакерские» карты, где данные в этом блоке можно изменить.

❈ ❈ ❈

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

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

  • записать в память терминала 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 можно только записать, но нельзя прочитать, эти байты всегда возвращаются заполненными нулями.

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

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

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

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

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

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

example-04: Чтение карты Mifare Classic

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

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

Итак, мы инициировали соединение с библиотекой, прочитали список терминалов, выбрали первый и подключились к поднесённой карте.

В этом примере я буду APDU команды писать в виде строкового литерала и копировать в буфер функцией memcpy(). Выглядит это примерно так:

memcpy(send_buffer, "\x01\x02", 2);

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

Первый этап: сохранение ключа в памяти терминала, это делается всё той же функцией SCardTransmit() для передачи APDU:

LONG send_buffer_size;
BYTE send_buffer[100];
const LONG recv_buffer_size = 100;
BYTE recv_buffer[recv_buffer_size];

//                  CLA    INS    P1     P2     Lc     Command bytes
memcpy(send_buffer, "\xff" "\x82" "\x00" "\x00" "\x06" "\xff\xff\xff\xff\xff\xff", send_buffer_size);
DWORD recv_length = recv_buffer_size;

result = SCardTransmit(reader, SCARD_PCI_T1, 
    send_buffer, send_buffer_size, NULL,
    recv_buffer, &recv_length);

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

  • CLA = FF
  • INS = 82, команда Load Keys
  • P1 = 00, структура ключа, набор битов, в данном случае это означает, что ключ сохраняется в волатильной памяти, то есть терминал его забудет после отключения питания, ACR122U другие режимы не поддерживает, другие модели терминалов могут сохранять ключ в постоянной памяти.
  • P2 = 0x00, номер ключа, для ACR122U здесь может стоять либо 00, либо 01. По сути это ссылка, которую мы передадим в следующей команде.
  • Lc = 06, длина данных для команды, длина ключа 6 байтов
  • собственно данные ключа (мы используем стандартный, который производитель зашил в карту при изготовлении).

Эта команда не передаёт данные на карту, она исключительно для терминала.

Коды ошибок определены в спецификации PC/SC part 3, раздел 3.2.2.1.4, вы должны их все обрабатывать, если пишете нормальную программу. В нашем примере я обрабатываю только код успешного завершения 0x9000:

if (SW != 0x9000) {
    printf("Failed to set keys\n");
    printf("Response APDU: ");
    print_bytes(recv_buffer, recv_length);
    SCardDisconnect(reader, SCARD_RESET_CARD);
    SCardReleaseContext(sc_context);
    return 1;
}

❈ ❈ ❈

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

recv_length = recv_buffer_size;
send_buffer_size = 10;

//                  CLA    INS    P1     P2     Lc     Command bytes
memcpy(send_buffer, "\xff" "\x86" "\x00" "\x00" "\x05" "\x01\x00\x00\x60\x00", send_buffer_size);
result = SCardTransmit(reader, SCARD_PCI_T1, 
    send_buffer, send_buffer_size, NULL,
    recv_buffer, &recv_length);

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

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

Статус успешно завершённой операции — 0x9000. Коды ошибок определены в спецификации PC/SC part 3, раздел 3.2.2.1.6, вы должны их все обрабатывать, если пишете нормальную программу. Однако тот же ACR122U у меня на любую ошибку возвращает всегда статус 0x6300, так что поддержка PC/SC у конкретного девайса не всегда соответствует спецификации.

❈ ❈ ❈

Третий этап — чтение данных. В нашем примере мы хотим прочитать нулевой блок (для которого в предыдущей команде проводили аутентификацию).

recv_length = recv_buffer_size;
send_buffer_size = 5;
//                  CLA    INS    P1     P2     Le
memcpy(send_buffer, "\xff" "\xb0" "\x00" "\x00" "\x10", send_buffer_size);

result = SCardTransmit(reader, SCARD_PCI_T1, 
    send_buffer, send_buffer_size, NULL,
    recv_buffer, &recv_length);
  • CLA = FF
  • INS = B0, команда Read Binary
  • P1 = 00 MSB номера блока
  • P2 = 00 LSB номера блока
  • Le = 10, размер ожидаемого результата, 16 байтов

Если мы правильно угадали с ключом, терминал должен вернуть R-APDU с байтами нулевого блока. В случае моей карты это выглядит так:

% make && ./example-04
cd ../util && make
make[1]: `libutil.a' is up to date.
cc -Wall -c -o example-04.o example-04.c
cc -o example-04 example-04.o   ../util/libutil.a -framework PCSC
Connection to PC/SC established
Use reader 'ACS ACR122U'
Card connected, protocol to use: T1.
Block bytes: 9C 9C AC 99 35 08 04 00 01 51 BE 34 B3 D0 FB 1D
Connection to PC/SC closed

В этом блоке находится записанная производителем системная информация о карте, например, UID.

Вы можете поэкспериментировать с кодом, например, чтобы вернуть содержимое второго блока (с адресом 0x0001), отправьте команду с таким APDU:

//                  CLA    INS    P1     P2     Le
memcpy(send_buffer, "\xff" "\xb0" "\x00" "\x01" "\x10", send_buffer_size);

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

Производитель │  Ключ 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

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

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

example-05: Разбор данных на карте Mifare Classic

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

В этом примере мы попытаемся прочитать всю карту.

Этот и все последующие примеры написаны на языке C++ вместо C, а вся работа с PC/SC инкапсулирована в отдельную библиотеку xpcsc, лежащую рядом с примерами. C++ — язык более высокого уровня, чем C и в нём код получается более понятным и выразительным. Сам код библиотеки я описывать в статье не буду, там нет ничего сложного, всего лишь удобная обёртка для низкоуровневых вызовов PC/SC плюс несколько сервисных классов, функций и макросов.

Я специально не стал уходить глубоко в абстракции — вся логика работы с новыми объектами примерно соответствует логике библиотеки pcsc. Однако вся нудная и многословная часть (типа подготовки байтовых массивов и работа с памятью) максимально скрыта.

Чтобы читать код, вам нужно знать C++.

Для операций с байтами и байтовыми буферами я использую тип xpcsc::Bytes, определённый как std::basic_string<xpcsc::Byte>. В свою очередь xpcsc::Byte определён как синоним для uint8_t.

Задача:

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

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

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

❈ ❈ ❈

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

typedef enum {Key_None, Key_A, Key_B} CardBlockKeyType;
struct AccessBits {
    xpcsc::Byte C1 : 1, C2 : 1, C3 :1;
    bool is_set;
    AccessBits() : is_set(false) {};
};
struct CardContents {
    xpcsc::Byte blocks_data[64][16];
    xpcsc::Byte blocks_keys[64][6];

    CardBlockKeyType blocks_key_types[64];
    AccessBits blocks_access_bits[64];
};

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

  • проходим по всем блокам и заполняем структуру типа CardContents;
  • красиво печатаем содержимое этой структуры.

Все операции с библиотекой PC/SC инкапсулированы в класс xpcsc::Connection, инициализация происходит так:

xpcsc::Connection c;

try {
    c.init();
} catch (xpcsc::PCSCError &e) {
    std::cerr << "Connection to PC/SC failed: " << e.what() << std::endl;
    return 1;
}

Получение списка терминалов и подключение к первому делаются так же просто:

std::vector<std::string> readers = c.readers();
if (readers.size() == 0) {
    std::cerr << "[E] No connected readers" << std::endl;
    return 1;
}

xpcsc::Reader reader;

try {
    std::string reader_name = *readers.begin();
    std::cout << "Found reader: " << reader << std::endl;
    eader = c.wait_for_reader_card(reader_name);
} catch (xpcsc::PCSCError &e) {
    std::cerr << "Wait for card failed: " << e.what() << std::endl;
    return 1;
}

Здесь переменная типа xpcsc::Reader — это хэндл, который нужен далее для всех операций с терминалом.

❈ ❈ ❈

Дальше получаем и парсим ATR классом xpcsc::ATRParser, заодно проверяем, что у нас в терминале именно бесконтактная карта:

xpcsc::Bytes atr = c.atr(reader);
std::cout << "ATR: " << xpcsc::format(atr) << std::endl;

// parse ATR
xpcsc::ATRParser p;
p.load(atr);

if (!p.checkFeature(xpcsc::ATR_FEATURE_PICC)) {
    std::cerr << "Contactless card required!" << std::endl;
    return 1;
}

Также проверяем, что терминале карта Mifare Classic 1K:

if (!p.checkFeature(xpcsc::ATR_FEATURE_MIFARE_1K)) {
    std::cerr << "Mifare card required!" << std::endl;
    return 1;
}

Класс xpcsc::ATRParser инкапсулирует операции парсинга байтового массива ATR, описание этого алгоритма заслуживает отдельной статьи.

❈ ❈ ❈

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

Ключи задаются в коде следующим образом:

const size_t keys_number = 3;
xpcsc::Byte keys[keys_number][6] = {
    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},  // NXP factory default key
    {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5},  // Infineon factory default key A
    {0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5}   // Infineon factory default key B
};

Для всех операций в коде определены константы-шаблоны, на основе которых дальше создаются реальные команды:

// template for Load Keys command
const xpcsc::Byte CMD_LOAD_KEYS[] = {0xFF, 0x82, 0x00, 0x00, 
    0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

// template for General Auth command
const xpcsc::Byte CMD_GENERAL_AUTH[] = {0xFF, 0x86, 0x00, 0x00, 
    0x05, 0x01, 0x00, 0x00, 0x60, 0x00};

// template for Read Binary command
const xpcsc::Byte CMD_READ_BINARY[] = {0xFF, 0xB0, 0x00, 0x00, 0x10};

А также байтовые буферы для запроса и ответа:

xpcsc::Bytes command;
xpcsc::Bytes response;

Вызов команды через объект xpcsc::Connection на основе этих констант делается так (на примере команды Load Keys):

command.assign(CMD_LOAD_KEYS, sizeof(CMD_LOAD_KEYS));
// заменяем 6 байтов начиная с пятого на 6 байтов из массива keys[k]
command.replace(5, 6, keys[k], 6); 
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    // здесь обработка ошибки
}

❈ ❈ ❈

Каждая проверка — это последовательный вызов команд Load Keys и General Auth, вторая команда делается так:

command.assign(CMD_GENERAL_AUTH, sizeof(CMD_GENERAL_AUTH));
// записываем в байт 7 адрес блока
command[7] = first_block;
// записываем в байт 8, какой ключ использовать (`61` — ключ B)
command[8] = 0x61;
c.transmit(reader, command, &response);
if (c.response_status(response) == 0x9000) {
    // здесь читаем данные, так как команда завершилась успешно
}

❈ ❈ ❈

После успешной аутентификации можно прочитать содержимое блока, делается это командой Read Binary, она описана в разделе 3.2.2.1.8 спецификации PC/SC part 3.

command.assign(CMD_READ_BINARY, sizeof(CMD_READ_BINARY));
// в байт 3 (это поле P2) записываем адрес блока
command[3] = block;
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    // обработка ошибки
}

Адрес блока указывается в аргументах команды P1 (старший байт) и P2 (младший байт). Так как у нас количество блоков меньше 256, мы используем только P2.

❈ ❈ ❈

Как вы уже заметили, тип xpcsc::Bytes очень удобен для манипуляции с байтами. Подробное его описание со всеми возможными методами можно прочитать в описании базового класса http://en.cppreference.com/w/cpp/string/basic_string.

❈ ❈ ❈

Информация об условиях доступа записана в трейлере сектора в байтах 6, 7 и 8. Для каждого блока условия доступа определяются тремя битами, их принято обозначать как C1, C2, C3. Эти биты определённым образом распределены по битовым представлениям байтов 6, 7 и 8. Всего нам нужно 4×3=12 битов, они хранятся в 24 битах (8×3=24) два раза: в прямом и инвертированном видах. Теоретически, при чтении трейлера вы должны проверять целостность, чтобы соответствующие биты на инвертированной и прямой позициях дополняли друг друга, но я здесь этого не делаю.

Сочетания битов имеют разный смысл для обычных блоков (первых трёх) и трейлера. Ниже две таблицы, как их интерпретировать, они взяты (в несколько модифицированном виде) из технической документации к карте 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 и так далее.

Сочетание битов 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(A) означает, что можно ключом A перезаписать ключ A, AccessRead(A) можно читать байты контроля доступа ключом A и так далее.

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

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

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

example-06: Персонализация карты Mifare Classic 1K

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

Цель этого примера: продемонстрировать запись данных на карту.

Персонализацией карты называется процесс её подготовки для реального использования:

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

Вам понадобится чистая «фабричная» карта Mifare Classic 1K, если такой карты нет, подойдёт любая другая с одним чистым (свободным) сектором. Найти чистый сектор вам поможет программа из предыдущего примера.

Мы напишем сразу четыре программы: для персонализации (активации) карты, для «использования» (списывания с баланса при каждом прикосновении к терминалу), для проверки баланса, для «сброса» (восстановления изначального состояния).

Будем эмулировать «электронный проездной»:

  • «активация» (персонификация) чистой карты;
  • «пополнение» карты в процессе активации;
  • просмотр баланса карты;
  • «списывание» фиксированной суммы при использовании карты.

Примерный сценарий работы готового проекта:

  • запускаем программу tcard-activate и подносим чистую карту к терминалу;
  • карта «активируется» и пополняется на 15 000 у.е.;
  • останавливаем tcard-activate и запускаем tcard-use;
  • теперь при каждом прикосновении карты к терминалу с неё будет списываться 170 у.е. и печататься остаток доступных средств.

Если запустить программу tcard-balance, то будет напечатан текущий баланс с карты.

Все нужные данные будем хранить в одном блоке на карте. Сумму будем записывать 16-битным целым без знака (два байта) в первых двух байтах блока. Условия доступа на блок менять не будем.

Для «деактивации» карты будем использовать программу tcard-deactivate — она очищает блок с балансом и меняет ключи для этого сектора на изначальные.

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

Планирование данных

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

const uint8_t CARD_SECTOR  = 0x5;
const uint8_t CARD_SECTOR_BLOCK = 0x2;
const uint8_t CARD_BLOCK = ((CARD_SECTOR * 4) + CARD_SECTOR_BLOCK);
const uint8_t CARD_SECTOR_TRAILER = ((CARD_SECTOR * 4) + 3);
const uint8_t DEFAULT_KEY_A[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
const uint8_t ACTIVE_KEY_A[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
const uint16_t INITIAL_BALANCE = 15000;
const uint16_t TICKET_PRICE = 170;

Используем третий (0x2) блок шестого (0x5) сектора (его полный адрес вычисляется в константе CARD_BLOCK), «фабричный» ключ храним в константе DEFAULT_KEY_A, активный ключ — в константе ACTIVE_KEY_A. Вы можете вместо этих параметров прописать собственные, если они отличаются от моих.

Также в этом файле определим константные байтовые массивы с шаблонами команд, они общие для всех программ:

// template for Load Keys command
const xpcsc::Byte CMD_LOAD_KEYS[] = {0xFF, 0x82, 0x00, 0x00, 
    0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

// template for General Auth command
const xpcsc::Byte CMD_GENERAL_AUTH[] = {0xFF, 0x86, 0x00, 0x00, 
    0x05, 0x01, 0x00, 0x00, 0x60, 0x00};

// template for Read Binary command
const xpcsc::Byte CMD_READ_BINARY[] = {0xFF, 0xB0, 0x00, 0x00, 0x10};

// template for Update Binary command
unsigned char CMD_UPDATE_BINARY[] = {0xFF, 0xD6, 0x00, 0x00, 0x10,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

Эти байтовые массивы будут использоваться для инициализации команд в объектах типа xpcsc::Bytes.

Новая команда, которую мы будем использовать в этом примере — Update Binary, она подробно описана в разделе 3.2.2.1.9 спецификации PC/SC part 3.

tcard-activate

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

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

command.assign(CMD_LOAD_KEYS, sizeof(CMD_LOAD_KEYS));
// заменяем 6 байтов, начиная с байта 5, на значение ключа
command.replace(5, 6, DEFAULT_KEY_A, 6);
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Failed to load key" << std::endl;
    return 1;
}

// аутентифицируемся ключом A, чтобы получить доступ к блоку CARD_BLOCK
command.assign(CMD_GENERAL_AUTH, sizeof(CMD_GENERAL_AUTH));
command[7] = CARD_BLOCK;
command[8] = 0x60;
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Cannot authenticate using DEFAULT_KEY_A!" << std::endl;
    return 1;
}

Дальше считываем нужный блок и проверяем, что он содержит только нули — мы активируем чистую «фабричную» карту!

command.assign(CMD_READ_BINARY, sizeof(CMD_READ_BINARY));
command[3] = CARD_BLOCK;
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Cannot read block!" << std::endl;
    return 1;
}

for (size_t i = 0; i < 15; i++) {
    if (response.at(i) != 0) {
        std::cerr << "Block must be filled with zeroes!" << std::endl;
        return 1;
    }
}

Формируем в байтовом массиве блок с балансом (из константы INITIAL_BALANCE) в первых двух байтах и записываем его на карту командой Update Binary:

// первоначальный баланс
uint16_t balance = INITIAL_BALANCE;

// создаём пустой блок с нулями
xpcsc::Bytes balance_block(16, 0);
// записываем баланс в первые два байта
balance_block.replace(0, 2, (xpcsc::Byte*)&balance, 2);

// записываем весь блок на карту
command.assign(CMD_UPDATE_BINARY, sizeof(CMD_UPDATE_BINARY));
command[3] = CARD_BLOCK;
// копируем 16 байтов блока в команду
command.replace(5, 16, balance_block.c_str(), 16);
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Cannot update block!" << std::endl;
    return 1;
}

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

// читаем блок-трейлер
command.assign(CMD_READ_BINARY, sizeof(CMD_READ_BINARY));
command[3] = CARD_SECTOR_TRAILER;
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Cannot read block!" << std::endl;
    return 1;
}

// вытаскиваем 16 байтов блока из ответа
xpcsc::Bytes trailer = response.substr(0, 16);
// записываем ключ A
trailer.replace(0, 6, ACTIVE_KEY_A, 6);
// и ключ B
trailer.replace(10, 6, ACTIVE_KEY_A, 6);

// обновляем блок-трейлер на карте
command.assign(CMD_UPDATE_BINARY, sizeof(CMD_UPDATE_BINARY));
command[3] = CARD_SECTOR_TRAILER;
command.replace(5, 16, trailer.c_str(), 16);
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Cannot update block!" << std::endl;
    return 1;
}

Этот шаг самый опасный: если вы случайно повредите блок с условиями доступа, то можете навсегда потерять доступ ко всему сектору!

❈ ❈ ❈

После запуска программа ждёт карту с чистым блоком и пытается записать этот блок и также сменить ключи. Затем программа сама завершается, в случае успеха пишет Card activated!, в случае неудачи — печатает сообщение об ошибке.

tcard-balance

Напишем программу для проверки баланса. Она совсем простая, весь «активный» код выглядит так:

// загружаем ACTIVE_KEY_A
command.assign(CMD_LOAD_KEYS, sizeof(CMD_LOAD_KEYS));
command.replace(5, 6, ACTIVE_KEY_A, 6);
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Failed to load key" << std::endl;
    return 1;
}

// аутентифицируемся ключом A, чтобы получить доступ к блоку CARD_BLOCK 
command.assign(CMD_GENERAL_AUTH, sizeof(CMD_GENERAL_AUTH));
command[7] = CARD_BLOCK;
command[8] = 0x60;
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Cannot authenticate using ACTIVE_KEY_A!" << std::endl;
    return 1;
}

// читаем CARD_BLOCK
command.assign(CMD_READ_BINARY, sizeof(CMD_READ_BINARY));
command[3] = CARD_BLOCK;
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Cannot read block!" << std::endl;
    return 1;
}

// извлекаем баланс из первых двух байтов
uint16_t balance = 0;
memcpy(&balance, response.c_str(), 2);

// печатаем баланс
std::cout << "Card balance is: " << balance << std::endl;

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

% ./tcard-balance
Found reader: ACS ACR122U
Card balance is: 15000

А на не активированной карте — так:

% ./tcard-balance
Cannot authenticate using ACTIVE_KEY_A!

tcard-use

Эта программа работает так: при каждом прикосновении карты к терминалу, с карты списывается фиксированная сумма у.е. Все наши предыдущие команды завершались сразу после выполнения операции. Однако в этой нам необходимо постоянно «слушать» терминал и отслеживать добавление и убирание карты, это несколько меняет логику работы и я расскажу о структуре этой программы подробнее.

Начинается всё стандартно: подключение библиотеки, чтение списка терминалов, получение имени первого терминала в переменной reader_name. А вот дальше всё по-другому.

Вся внутренняя логика обёрнута вот в такой код:

try {
    while (1) {
        // вся логика здесь
    }
} catch (xpcsc::PCSCError &e) {
    std::cerr << "PC/SC operation failed: " << e.what() << std::endl;
    return 1;
}

Внутри бесконечного цикла while(1) {} необходимо сделать следующее:

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

Я подробно расскажу о каждом из этих пунктов.

остановить выполнение кода до появления карты в терминале

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

c.wait_for_card_remove(reader_name);
std::cout << "Terminal is ready, use your card!" << std::endl;
xpcsc::Reader reader = c.wait_for_reader_card(reader_name);

Метод wait_for_card_remove() класса xpcsc::Connection останавливает выполнение кода, если в терминал вставлена карта и завершается, как только карта извлечена. Если карты в терминале нет, то завершается сразу.

убедиться, что подключена карта нужного типа

Здесь также всё стандартно за единственным исключением — если подключена неподдерживаемая карта, мы не завершаем программу, а уходим на начало цикла:

if (!p.checkFeature(xpcsc::ATR_FEATURE_PICC)) {
    std::cerr << "Contactless card required!" << std::endl;
    continue;
}

аутентифицироваться ключом ACTIVE_KEY_A

Здесь аналогично — в случае ожидаемых ошибок переходим на начало цикла, где ждём убирания карты.

прочитать текущий баланс из блока CARD_BLOCK

// читаем CARD_BLOCK
command.assign(CMD_READ_BINARY, sizeof(CMD_READ_BINARY));
command[3] = CARD_BLOCK;
c.transmit(reader, command, &response);
if (c.response_status(response) != 0x9000) {
    std::cerr << "Cannot read block!" << std::endl;
    continue;
}

// и читаем баланс в переменную balance
uint16_t balance = 0;
memcpy(&balance, response.c_str(), 2);

обновить баланс (если это можно сделать)

записать обновлённое содержимое блока назад на карту

Здесь мы проверяем, что на карте достаточно денег, и если мы можем «списать» сумму TICKET_PRICE, делаем это:

if (balance < TICKET_PRICE) {
    std::cout << "Not enough money on the card!" << std::endl;
} else {
    // обновляем баланс и обновляем содержимое байтового буфера блока
    balance -= TICKET_PRICE;
    xpcsc::Bytes balance_block(16, 0);
    balance_block.replace(0, 2, (unsigned char *)&balance, 2);

    // обновляем блок на карте
    command.assign(CMD_UPDATE_BINARY, sizeof(CMD_UPDATE_BINARY));
    command[3] = CARD_BLOCK;
    command.replace(5, 16, balance_block.c_str(), 16);
    c.transmit(reader, command, &response);
    if (c.response_status(response) != 0x9000) {
        std::cerr << "Cannot update block!" << std::endl;
        c.wait_for_card_remove(reader_name);
        continue;
    }
}

tcard-deactivate

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

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

  • Обязательно посмотрите, как реализованы методы wait_for_card_remove() и wait_for_reader_card() класса xpcsc::Connection.
  • Ключи к блокам сектора хранятся в трейлере сектора. Трейлер сектора записывается точно так же, как и любой другой блок.

❈ ❈ ❈

В этом примере я использовал только стандартные команды для работы с бесконтактными картами памяти. Эти команды описаны в спецификации PC/SC. Однако существуют и проприетарные, свои для каждого типа карт. Например, для карт Mifare Classic — это команды для работы с числовыми значениями в блоках. Каждый терминал реализует их по-своему и они описаны в технической документации к устройству.

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

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

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

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

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

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

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

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

Самый распространённый технический стандарт для банковских карт называется EMV, по первым буквам Europay, MasterCard, Visa. Документация стандарта доступна для свободного скачивания с официального сайта, на момент написания статьи последняя версия EMV 4.3 и скачать файлы можно отсюда: https://www.emvco.com/specifications.aspx?id=223.

Современное название смарт-карт для 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.

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

Коммуникация программы с картой происходит через терминал: отправка команд (C-APDU) и получение ответов (R-APDU).

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

Стандарт ISO/IEC 7816-4 описывает только команды для записи и чтения данных.

Совместимая с ISO/IEC 7816-4 смарт-карта предоставляет специальный интерфейс для доступа к данным на карте. Логически все они представлены объектами-файлами (files). Просто так писать и читать байты из файлов в общем случае невозможно. И вообще, не стоит воспринимать эти объекты как привычные файлы из linux/windows/macos, это объекты совершенно иной природы.

ISO/IEC 7816-4 определяет две группы (иерархичных) файлов:

elementary file (элементарный файл) / EF
Объект с данными, не может содержать в себе других объектов.
dedicated file (назначенный файл) / DF
Это нечто вроде каталога или контейнера, «внутри» DF могут храниться либо элементарные файлы (EF), либо другие назначенные файлы (DF). Неформально принятно DF называть каталогами (directories).
master file (главный файл) / MF
Это DF, находящийся на самом верху иерархии над всеми объектами.

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

❈ ❈ ❈

И ещё несколько терминов и сокращений:

internal elementary file (внутренний элементарный файл) / IEF
EF, данные в котором читает и пишет исключительно операционная система карты или карточные приложения. Например, в IEF хранятся секретные ключи для криптографии. IEF недоступны для внешнего приложения.
working elementary file (рабочий элементарный файл) / WEF
EF, данные в котором читает и пишет исключительно внешнее приложение через терминал. Операционная система или карточные приложения эти файлы не используют.
file identifier (идентификатор файла) / FID
Идентификатор файла (EF или DF), имеет размер в два байта. Для MF в ISO/IEC 7816-4 зарезервирован идентификатор 3F 00. Есть ещё несколько зарезервированных FID, о них я расскажу позднее.
short EF identifier (короткий идентификатор EF) / SFI
Специальный тип идентификаторов, используется только для элементарных файлов и состоит максимум из пяти битов. Этот идентификатор позволяет читать данные из файла без его предварительного выбора.
DF name (имя DF)
Каждому DF можно назначить имя — байтовый массив размером 16 байтов. В имени можно использовать любые значения для байтов, а не только ASCII-буквы.

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

В технической спецификации TS 102 221 для UICC (это SIM-карты) вводится новый тип файлов:

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

Пример: приложение для работы с MaserCard/Visa на банковской карте.

❈ ❈ ❈

Любой файл перед использованием нужно сначала выбрать. В ISO/IEC 7816 эта команда называется SELECT и описана в разделе 7.1.1 ISO/IEC 7816-4. Команда SELECT «привязывает» файл к указанному в байте CLA каналу, обычно это нулевой канал, но есть карты, поддерживающие несколько каналов. Номер канала задаётся битами b1 и b2. Байт INS — A4. Семантика параметров P1 и P2 детально описана в стандарте.

Для выбора можно использовать 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. Подробно об этих объектах я расскажу в последующих разделах и примерах кода.

❈ ❈ ❈

После выбора EF с ним можно работать: читать, писать, удалять. Данные в файле могут существовать в одной из трёх форм (русскоязычная терминология даётся по официальному переводу ГОСТ Р ИЗО:МЭК 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.

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

Информационный объект — это структурированные данные, закодированные в массив байтов. Стандарт ISO/IEC 7816-4 определяет два вида кодирования: SIMPLE-TLV и BER-TLV.

SIMPLE-TLV

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

Сначала идёт один байт с тегом, это значение от 01 до FE (00 и FF не допускаются).

Затем идёт один или три байта, задающих длину блока данных. Если первый байт имеет значение от 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 — есть только тег, длина блока данных нулевая

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

BER-TLV

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

Также в BER-TLV определена семантика для некоторых элементов.

❈ ❈ ❈

Размер тега может быть 1, 2 или 3 байта.

Биты b8 и b7 первого байта определяют класс информационного объекта:

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

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

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

Биты первого байта с 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).

Обратите внимание, что тег и номер тега — это разные вещи!

Вот два примера: теги 6F и 9F38.

BER-TLV tags example

❈ ❈ ❈

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

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

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

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

❈ ❈ ❈

Вот несколько примеров кодирования данных через 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 описан в разделе 5.2.2 стандарта ISO/IEC 7816-4, а также в разделе 8.1.2 стандарта ASN.1 ISO/IEC 8825-1.

example-07: Чтение данных из банковской карты

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

Код примера: https://github.com/sigsergv/pcsc-tutorial/blob/master/example-07/example-07.cpp

Банковские карты подчиняются стандарту EMV, его полная спецификация доступна по адресу https://www.emvco.com/specifications.aspx?id=223. Все документы разбиты на книги (books), каждая из книг отвечает за определённый аспект.

Каждая платёжная система должна работать на карте внутри собственного приложения с уникальным идентификатором (напомню, что идентификатором приложения — AID — является имя ADF — главного DF приложения). Обычно на карте есть только одно приложение для одной платёжной системы.

На карте может присутствовать специальный DF с именем 1PAY.SYS.DDF01, по EMV он называется PSE (Payment System Environment), в этом каталоге хранится список идентификаторов установленных на карте платёжных приложений (необязательно всех, но указанные в нём приложения должны быть международными/глобальными). Наличие этого DF не гарантируется. Если терминал не обнаруживает PSE, он пытается перебрать все поддерживаемые им идентификаторы. Вся процедура использования PSE описана в разделе 12.3.2 книги 1 EMV_v4.3.

C-APDU для выбора DF с именем 1PAY.SYS.DDF01 выглядит так (команда SELECT):

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

Соответствующий C++ код:

command.assign(xpcsc::parse_apdu("00 A4 04 00   0E  31 50 41 59 2E 53 59 53 2E 44 44 46 30 31"));
c.transmit(reader, command, &response);
if (c.response_status(response) == 0x9000) {
    // парсинг ответа, в моём случае это строка байтов (с исключённым статусным словом)
    // 6F 15 84 0E 31 50 41 59 2E 53 59 53 2E 44 44 46 30 31 A5 03 88 01 01
}

Здесь я использую функцию xpcsc::parse_apdu для парсинга строкового представления C-APDU в объект типа xpcsc::Bytes.

В P1 мы указываем 04, то есть выбираем DF по его имени. В P2 указываем 00, то есть хотим получить в ответе шаблон FCI (FCI template), FCI расшифровывается как File Control Information.

Первый байт 6F означает, что данные в ответе закодированы в BER-TLV и представляют собой шаблон FCI, при этом первый байт уже входит в данные и, следовательно, является тегом. В битовом представлении B6 выглядит так: 0110 1111, вы можете посмотреть в предыдущем разделе, что эти биты означают. Для нас важен бит b6, он выставлен в 1, значит, последующие данные также закодированы в BER-TLV.

Следующим байтом идёт длина, в нашем случае это 0x15 (21 в десятичной форме), именно такая длина у блока данных. После первого шага разбора структуру нашего ответа можно представить так:

tag: 6F (FCI template)
length: 0x15 (21 byte)
  84 0E 31 50 41 59 2E 53 59 53 2E 44 44 46 30 31 A5 03 88 01 01

Следующий тег — 84 (1000 0100), контекстно-зависимый класс, данные не закодированы. Длина — 0x0E, то есть 14 байтов. Однако после 14 байтов строка не заканчивается и продолжается блоком A5 03 88 01 01, вы его можете разобрать сами. После второго шага разбора структура такая:

tag: 6F (FCI template)
length: 0x15 (21 byte)
  tag: 84 (context-specific, primitive data encoding)
  length: 0E (14 bytes)
    31 50 41 59 2E 53 59 53 2E 44 44 46 30 31
  tag: A5 (context-specific, BER-TLV encoding)
  length: 03 (3 bytes)
    tag: 88 (context-specific, primitive data encoding)
    length: 01
      01

Это полностью разобранное BER-TLV выражение. Дальше нам нужно понять, что скрывается за этими тегами. Их семантика для FCI определена в Таблице 12 и разделе 5.3 стандарта ISO/IEC 7816-4.

Тег 84 обозначает имя DF, оно совпадает с тем, которое мы передали в команде SELECT.

Тег A5 обозначает проприетарную информацию, закодированную в BER-TLV. Смотрим в спецификацию EMV, а конкретно — в раздел 11.3.4 книги 1 EMV_v4.3. Данные в блоке с тегом 88 содержат SFI (короткий идентификатор) элементарного файла. Именно в этом файле находятся непосредственно идентификаторы платёжных приложений, в нашем случае SFI=01.

Поле с тегом 88 является обязательным. Помимо него EMV определяет несколько дополнительных опциональных полей:

  • 5F2D — Language Preference
  • 9F11 — Issuer Code Table Index
  • BF0C — FCI Issuer Discretionary Data

❈ ❈ ❈

Мы получили SFI=1 — короткий идентификатор файла Payment System Directory, это EF, содержащий до 10 записей, каждая из которых определяет платёжное приложение (или несколько, стандарт допускает несколько элементов в одной записи). Эти записи читаются командой READ RECORD (см. раздел 7.3.3 стандарта ISO/IEC 7816-4). C-APDU выглядит так:

CLA  INS  P1    P2     Le
00   B2   [P1]  [P2]   00

P1 — это номер записи, мы последовательно проверяем значения от 1 до 10.

P2 — это специальным образом сформированное значение из SFI, детали этого процесса описаны в ISO/IEC 7816-4 (раздел 7.3.3, Таблица 49), а здесь я приведу готовую формулу: (SFI << 3) | 4, то есть в битовом представлении байта в первых пяти битах записывается SFI, а в последних трёх — дополнительные параметры.

Далее последовательно отправляем C-APDU: 00 B2 01 P2 00, 00 B2 02 P2 00, 00 B2 03 P2 00 и так далее, увеличивая при каждом запросе третий байт (т.е. P1) на единицу. От каждой из команд мы ожидаем один из трёх статусов:

  • 9000 — команда успешно завершилась и вернула запись с информацией о платёжном приложении.
  • 6CXX — команда завершилась с ошибкой, она означает, что запись есть, но необходимо в C-APDU передать точное значение длины ожидаемого ответа (Le), эта точная длина указана в байте XX. В ответ на такой статус нужно повторить предыдущую команду, только вместо 00 в последнем байте указать значение из XX.
  • 6A83 — больше записей в файле нет, перестаём отправлять запросы.

R-APDU в случае успешного завершения выглядит примерно так (без статусных байтов 90 00):

70 19 61 17 4F 07 A0 00 00 00 03 10 10 50 0C 56 69 73 61 20 43 6C 61 73 73 69 63

Формат этой записи описан в разделе 12.2.3 книги 1 EMV_v4.3, вот она в разобранном виде (это тоже BER-TLV):

tag: 70 (Payment System Directory record)
length: 0x19 (25 bytes)
  tag: 61
  length: 0x17 (23 bytes)
    tag: 4F (ADF Name)
    length: 0x07
      A0 00 00 00 03 10 10
    tag: 50 (Application Label)
    length: 0x0C (12 bytes)
      56 69 73 61 20 43 6C 61 73 73 69 63 (ASCII: "Visa Classic")

Здесь нас особо интересует поле с тегом 4F, в нём содержится имя ADF с банковским приложением. Одновременно это имя является идентификатором приложения AID. В нашем случае A0 00 00 00 03 10 10 является идентификатором приложения Visa Credit/Debit. несколько AID других платёжных систем в таблице ниже:

 A0 00 00 00 03 10 10  │ Visa Classic Credit/Debit
───────────────────────┼──────────────────────────
 A0 00 00 00 03 20 10  │ Visa Electron
───────────────────────┼──────────────────────────
 A0 00 00 00 04 10 10  │ MasterCard
───────────────────────┼──────────────────────────
 A0 00 00 00 25 00 00  │ American Express

В репозитории описанная процедура оформлена в виде функции read_apps_from_pse(), вы можете посмотреть код здесь https://github.com/sigsergv/pcsc-tutorial/blob/master/example-07/example-07.cpp.

Код парсера BER-TLV здесь: https://github.com/sigsergv/pcsc-tutorial/blob/master/libxpcsc/src/bertlv.cpp.

❈ ❈ ❈

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

Выбор DF приложения делается так же, как и выбор DF PSE — через команду SELECT. Продолжим с AID A0 00 00 00 03 10 10, C-APDU для выбора приложения будет выглядеть так:

CLA  INS  P1 P2   Lc  Data
00   A4   04 00   07  A0 00 00 00 03 10 10

Соответствующий код на C++ может выглядеть так:

// xpcsc::Bytes aid; определён где-то выше и содержит нужный AID

// формируем команду, сначала заголовок
command.assign(xpcsc::parse_apdu("00  A4  04 00"));
// дальше передаём байт с размером дополнительных данных
command.push_back(static_cast<xpcsc::Byte>(aid.size()));
// и добавляем собственно данные
command.append(aid);

c.transmit(reader, command, &response);

// дальше обрабатываем response

Эта команда также возвращает FCI. В нашем примере в разобранном виде FCI выглядит так (я убрал блоки с указанием длины и оставил только значимые данные):

  Tag: 6F
    Tag: 84
    Data: A0 00 00 00 03 10 10
    Tag: A5
      Tag: 50
      Data: 56 69 73 61 20 43 6C 61 73 73 69 63
      Tag: 9F 38
      Data: 9F 1A 02

Как обычно, информация о приложении находится в блоке с проприетарными данными (блок с тегом A5).

В поле с тегом 50 записано человекочитаемое имя приложения, в нашем случае это Visa Classic.

В поле с тегом 9F38 находится PDOL — Processing Options Data Object List, данные из этого поля используются для инициализации финансовой транзакции.

Полностью все поля описаны в Таблице 45 книги 1 EMV_v4.3.

❈ ❈ ❈

После выбора платёжного приложения начинается финансовая транзакция (Financial Transaction) — это отдельный набор команд и алгоритмов, описанный в книге 3 EMV_v4.3.

Первой командой в финансовой транзакции является GET PROCESSING OPTIONS, она описана в разделе 6.5.8 книги 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, они закодированы через BER-TLV, схема этой структуры описана в разделе 5.4 книги 3 EMV_v4.3.

Для конструирования объекта в поле Data нам понадобится PDOL, полученный на этапе выбора приложения.

Если этого поля в FCI ADF не было, то передаём Lc=02 и 83 00, то есть C-APDU: 80 A8 00 00 02 83 00 00.

Если поле PDOL есть, то мы должны на его основе сформировать блок данных (назовём его PDOL-DATA). Структура 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}

PDOL-DATA формируется только из соответствующих данных (без тегов и длин), идущих в том же порядке, что элементы в PDOL. Перед данными указывается тег 83 и байт с общей длиной, равной {L1} + {L2} + ... + {Ln}. Если программа не знает тег или не хочет его заполнять, можно передать заполненный нулями массив. Мы именно так и поступим.

❈ ❈ ❈

Результат выполнения команды GET PROCESSING OPTIONS может быть двух типов: структурированный (закодированный через BER-TLV, начинается с тега 77) или примитивный (начинается с тега 80), в виде байтовой строки. В обоих случаях он содержит два значения: AIP и AFL:

  • 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 │ 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
  Tag: 82 ; AIP
  Data: 39 00

  Tag: 94 ; AFL
  Data: 28 01 03 01 30 01 02 00

В AIP данные закодированы битами, их структура описана в Таблице 37 книги 3 EMV_v4.3.

Структура AFL описана в разделе 10.2 книги 3 EMV_v4.3. Это список идущих подряд четвёрок байтов, внутри каждой четвёрки байты имеют следующую семантику:

  • первые пять битов первого байта задают SFI;
  • второй байт задаёт номер первой записи, которую нужно прочитать из файла, определяемого SFI;
  • третий байт задаёт номер последней записи, которую нужно прочитать (включительно). Он больше либо равен второму.
  • четвёртый байт определяет количество записей, вовлеченных в офлайновую аутентификацию данных (эта тема описана в книге 2 EMV_v4.3.) Эти записи начинаются с той, которая указана во втором байте.

Терминал обязан прочитать все указанные файлы и записи в том порядке, в каком они перечислены в AFL. Записи читаются командой READ RECORD. Каждая запись представляет собой 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
│  ┌──────── номер первой записи для SFI
│  │  ┌───── номер последней записи для SFI
│  │  │  ┌── количество записей, необходимых для аутентификации карты
28 01 03 01

Это блок описывает три записи, в файле с SFI=05, для чтения этих записей нужно выполнить три команды с такими C-APDU:

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.

Аналогично разбирается вторая четвёрка.

После чтения и разбора каждой записи имеет смысл занести полученные значения в hash/map, чтобы было удобнее их потом доставать. На карте в открытом виде хранится довольно много данных, вот, например, список элементов данных с одной из моих старых карт (это результат работы примера из этого раздела):

Application label: MasterCard
Card capabilities:
  SDA supported: false
  DDA supported: false
  Cardholder verification supported: true
  Terminal risk management is to be performed: true
  Issuer authentication is supported: true
  CDA supported: false
5F 25 => Application Effective Date
5F 24 => Application Expiration Date
9F 07 => Application Usage Control
5A => Application Primary Account Number (PAN)
5F 34 => Application Primary Account Number (PAN) Sequence Number
8E => Cardholder Verification Method (CVM) List
9F 0D => Issuer Action Code - Default
9F 0E => Issuer Action Code - Denial
9F 0F => Issuer Action Code - Online
5F 28 => Issuer Country Code
9F 4A => Static Data Authentication Tag List
8C => Card Risk Management Data Object List 1 (CDOL1)
8D => Card Risk Management Data Object List 2 (CDOL2)
57 => Track 2 Equivalent Data
5F 20 => Cardholder Name
9F 08 => Application Version Number
9F 42 => Application Currency Code
8F => Certification Authority Public Key Index
9F 32 => Issuer Public Key Exponent
92 => Issuer Public Key Remainder
90 => Issuer Public Key Certificate
9F 49 => Dynamic Data Authentication Data Object List (DDOL)
9F 47 => ICC Public Key Exponent
9F 46 => ICC Public Key Certificate

Card number: XX XX XX XX XX XX XX XX
Card holder name: STOLYAROV/SERGEY
Effective date: 11 09 01
Exp date: 16 03 31

Заключение

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

Все комментарии можно писать как напрямую мне на email, так и оставлять прямо к этому посту.

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

Ссылки россыпью

Вот список ссылок, они мне очень помогли в работе над текстом.

https://www.eftlab.co.uk/index.php/site-map/knowledge-base
Огромная база по идентификаторам различных объектов.
https://www.eftlab.co.uk/index.php/site-map/knowledge-base/171-atr-list-full
Complete list of ATR
https://www.eftlab.co.uk/index.php/site-map/knowledge-base/211-emv-aid-rid-pix
Complete list of ISO AIDs
https://www.eftlab.com.au/index.php/site-map/knowledge-base/212-emv-rid
Complete list of RID
http://www.gorferay.com/structure-and-coding-of-the-application-identifier-aid/
Structure and coding of the application identifier (AID)
Smart card programming by Ugo Chirico
https://books.google.ru/books?id=jL0IBgAAQBAJ
Smart Card Handbook by Wolfgang Rankl and Wolfgang Effing
http://eu.wiley.com/WileyCDA/WileyTitle/productCd-0470743670.html
Smart card howto
http://www.tldp.org/HOWTO/pdf/Smart-Card-HOWTO.pdf
Пример сеанса с банковской картой
http://nicolas.riousset.com/category/software-methodologies/example-of-an-emv-dialog-for-an-interacflash-transaction/
GlobalPlatformPro — инструментарий для управления JavaCard-совместимыми картами
https://javacard.pro/globalplatform/

Стандарты и спецификации

Вот список стандартов, имеющих прямое отношение к теме статьи.

  • ISO/IEC 7816-3 Identification cards — Integrated circuit cards — Part 4: Organization, security and commands for interchange
  • ISO/IEC 7816-4 Identification cards — Integrated circuit cards — Part 3: Cards with contacts — Electrical interface and transmission protocols
  • ГОСТ Р ИСО:МЭК 7816-3-2013 Карты идентификационные. КАРТЫ НА ИНТЕГРАЛЬНЫХ СХЕМАХ. Часть 3: Карты с контактами. Электрический интерфейс и протоколы передачи // http://docs.cntd.ru/document/1200110392
  • ГОСТ Р ИСО:МЭК 7816-4-2013 Карты идентификационные. КАРТЫ НА ИНТЕГРАЛЬНЫХ СХЕМАХ. Часть 4: Организация, защита и команды для обмена // http://docs.cntd.ru/document/1200110393
  • ГОСТ Р ИСО/МЭК 14443-2-2014 Карты идентификационные. Карты на интегральных схемах бесконтактные. Карты близкого действия. Часть 2. Радиочастотный энергетический и сигнальный интерфейс // http://docs.cntd.ru/document/1200118651
  • ГОСТ Р ИСО/МЭК 14443-3-2014 Карты идентификационные. Карты на интегральных схемах бесконтактные. Карты близкого действия. Часть 3. Инициализация и антиколлизия // http://docs.cntd.ru/document/1200118652
  • ГОСТ Р ИСО/МЭК 14443-4-2014 Карты идентификационные. Карты на интегральных схемах бесконтактные. Карты близкого действия. Часть 4. Протокол передачи // http://docs.cntd.ru/document/1200118653
  • PC/SC part 1 Interoperability Specification for ICCs and Personal Computer Systems, Part 1. Introduction and Architecture Overview // http://pcscworkgroup.com/Download/Specifications/pcsc1_v2.01.01.pdf
  • PC/SC part 3 Interoperability Specification for ICCs and Personal Computer Systems, Part 3. Requirements for PC-Connected Interface Devices // http://pcscworkgroup.com/Download/Specifications/pcsc3_v2.01.09.pdf
  • EMV — Integrated Circuit Card Specifications for Payment Systems, Version 4.3, Book 1 — Application Independent ICC to Terminal Interface Requirements // https://www.emvco.com/download_agreement.aspx?id=652
  • EMV Integrated Circuit Card Specifications for Payment Systems, Version 4.3, Book 2 - Security and Key Management // https://www.emvco.com/download_agreement.aspx?id=653
  • EMV Integrated Circuit Card Specifications for Payment Systems, Version 4.3, Book 3 - Application Specification // https://www.emvco.com/download_agreement.aspx?id=654
  • EMV Integrated Circuit Card Specifications for Payment Systems, Version 4.3, Book 4 - Cardholder, Attendant, and Acquirer Interface Requirements // https://www.emvco.com/download_agreement.aspx?id=655

Комментарии

Раджа | 2017-04-25 в 18:56

Сам сказал писать предложения и замечания. :)

Это статья о смарт-картах и о том, как писать софт для работы с ними.

для понимания примеров кода (а также базовых Я бы тут скобки убрал и поставил запятую.

в первую очередь это linux и mac os x GNU/Linux, а то могут заметить. Mac OS по тексту гуляет во все стороны. надо узнать правильное название, а то Apple что-то там меняли.

| 2017-04-25 в 19:13

Ок, первые два поправил, про линукс оставил без изменений. Тут вам не лор.

Раджа | 2017-04-25 в 19:36

Ох как форматирование в комменте поехало!

Ну я потому и написал "могут".

Текст комментария (разметка: *курсив*, **полужирная**, [ссылка](http://example.com) или <http://example.com> ещё)
Имя (обязательно, 50 символов или меньше)
Email, на который получать ответы (не будет опубликован)
Веб-сайт
© 2006—2016 Sergey Stolyarov | Работает на Pyrone