Работаем со смарт-картами, используя Python (часть 1)
- четверг, 11 января 2018 г. в 03:14:10
Сначала, на момент задумки, в 2014 году, данная статья планировалась как единая публикация, но, проработав материал (лень вынудила растянуть этот процесс), я понял, что необходимо её разделить на две части:
Думаю, для профи-карточников первая часть будет представлять бо́льший интерес, а вторая часть будет интересна, прежде всего, новичкам в этой теме (и будет иметь метку Tutorial).
Среди множества Python-библиотек, обзоры которых есть на Хабре, я не обнаружил pyscard — библиотеки для взаимодействия со смарт-картами.
В этой статье я постараюсь дать краткое описание основных фич pyscard, а на сладкое напишем простенький командный процессор, работающий с картой посредством APDU.
Прошу учесть, что для понимания того, как использовать эту библиотеку, и окружающей терминологии требуется знакомство со стандартом ISO 7816-4
или, хотя бы, GSM 11.11
. К GSM-стандарту проще получить официальный доступ, скачав его с сайта ETSI, впрочем и ISO 7816-4 (pdf, старенькая версия) гуглится, несмотря на то, что за него на оф. сайте хотят денег).
Pyscard существует с 2007 года и является кроссплатформенной (win/mac/linux) надстройкой над PC/SC API.
Мое рабочее окружение, где я использую pyscard — Windows7
Материал данной статьи я тестировал, в основном, на mac OS, но на Windows7 тоже погонял, в виртуалке. Должен отметить, что, в отличие от XP, «семерка» и, вероятно, «десятка», с настройками по умолчанию, «ставит палки в колеса» при работе с картой в ридере:
Эти факторы доставили мне много боли при переходе с XP, пока я их не победил. Как это сделать, расскажу во второй части.
Разработка начата под эгидой одного из ведущих (и на момент создания, и сейчас) игроков карточного рынка.
Поддерживаются обе ветки Python (2 и 3).
В рабочем окружении я использую связку pyscard + Python 2.7, но, для статьи, мне показалось правильным задействовать актуальную на сегодня ветку Python (3.6)
На мой взгляд библиотека pyscard спроектирована не особо pythonic и больше напоминает порт какого-то Java фреймворка, однако полезности её это не уменьшает, по крайней мере для меня, хотя имена модулей выглядят странно, конечно.
Точкой входа в библиотеку является пакет smartcard
.
Отдельно стоит упомянуть пакет smartcard.scard
, который отвечает за связь с карточным API операционной системы. Если не нужны все абстракции библиотеки, а только голый PC/SC
, то вам сюда. Мы же на нём подробно останавливаться не будем.
Установка pyscard возможна следующими способами:
pip install pyscard
) — подходит для систем, настроенных на сборку артефактов из исходников, используется swig (ок для mac и, возможно, linux)Pyscard имеет информативное руководство пользователя, которое доступно на официальном сайте, pydoc и примеры, поэтому не вижу смысла дублировать все это здесь. Вместо этого мы:
smartcard.scard
, а именно smartcard
;Пора уже сделать вброс порции кода, а то всё скучные вступительные «бубубу»...
from smartcard.CardRequest import CardRequest
cardrequest = CardRequest()
# метод waitforcard(), в нашем случае ждем любую карту
cardservice = cardrequest.waitforcard() # здесь выполнение будет приостановлено до помещения карты в ридер
APDU = [0xA0, 0xA4, 0, 0, 2] # Это команда SELECT из GSM 11.11
# smartcard.CardConnection.CardConnection является контекст-менеджером
with cardservice.connection as connection:
connection.connect()
#далее - обмен данными с картой
data, sw1, sw2 = connection.transmit(APDU)
Какие задачи решает (практически любая) программа, работающая со смарт-картами в ридере? А вот эти:
Замечу, что перечисленные задачи решает, например, прошивка мобильного телефона.
smartcard
В этом разделе все имена указаны относительно пакета smartcard
.
CardType
Позволяют нам указать точный тип карт, с которыми наше приложение собирается работать. Можно сделать так, чтобы наше приложение даже не реагировало на помещение в ридер карты, которая нам не подходит.
Примеры:
CardType.ATRCardType
(существует в библиотеке) — фильтрация карт по значению ATR. Наше приложение будет реагировать только на карты с определенным значением ATR.USIMCardType
(я нафантазировал, можно реализовать) — допустимыми картами являются только USIM, внутри проверяем возможность выбора USIM-приложения.
CardRequest
и его подклассыПозволяют свести воедино все требования нашего приложения, касающиеся установления связи с картой:
По умолчанию никаких ограничений в CardRequest
не ставится.
CardConnection
Канал коммуникации нашего приложения с картой, позволяет отправлять на карту APDU и получать ответ, ключевой метод здесь — transmit()
. Именно с его помощью происходит непосредственное взаимодействие нашего приложения с картой. Необходимо отметить, что метод transmit()
всегда возвращает триплет (кортеж), состоящий из:
None
, в зависимости от типа APDU, не все APDU возвращают данные)SW1
SW2
CardConnection
является контекст-менеджером, что добавляет удобства при его использовании.
CardConnectionDecorator
Слово «декоратор» используется здесь в том же контексте, что и в Java, а не в том, к которому привыкли Python-разработчики.
Позволяет придать особые свойства объекту CardConnection
. Библиотека предоставляет рабочие декораторы с говорящими названиями: ExclusiveConnectCardConnection
и ExclusiveTransmitCardConnection
. Лично я не ощутил эффекта от использования этих декораторов — если система (Windows) уж решила вклиниться со своими APDU в нашу сессию, то ни один из этих декораторов не спасет, но, возможно, я что-то не так делал.
System.readers()
Позволяет получить список подключенных к системе кардридеров и установить связь с картой в определенном ридере.
sw.ErrorChecker
, sw.ErrorCheckingChain
По умолчанию, в ходе обмена данными между картой и нашего приложением, никакие ошибочные значения StatusWord (SW1, SW2) не возбуждают исключений. Это можно изменить, задействовав потомков ErrorChecker
, которые:
sw.ErrorCheckingChain
CardConnection
и проверяют на отсутствие ошибок результат каждого вызова метода transmit()
.SW1
, SW2
.CardConnectionObserver
Присоединяются к экземпляру CardConnection
и получают информацию обо всех командных APDU и ответах карты, которые проходят через наблюдаемое соединение. Пример применения — ведение лога команд и ответов от карты.
Вооруженные таким знанием, мы вполне можем замахнуться на написание командного процессора, использующего описываемую библиотеку.
Не буду подробно останавливаться на модуле cmd, который любезно предоставляет нам стандартная библиотека, о нем уже писали здесь, перейду к реализации.
Весь исходный код процессора находится на гитхабе.
Пройдемся по главным моментам, не размениваясь на мелочи.
select_reader()
Возвращает первый ридер, подключенный к компьютеру или None
, если подключенных ридеров нет.
def select_reader():
"""Select the first of available readers.
Return smartcard.reader.Reader or None if no readers attached.
"""
readers_list = readers()
if readers_list:
return readers_list[0]
Есть вариант этой функции (зависит от модуля msvcrt, т.е. только для Windows), который позволяет выбрать ридер, если их в компьютере несколько.
Данный класс, помимо наследования от cmd.Cmd
, реализует интерфейс обладает поведением наблюдателя smartcard.CardMonitoring.CardObserver
reader — устройство чтения, с которым будем работать.
card — объект карта, потребуется нам, чтобы определить момент смены карты в ридере.
connection — канал передачи APDU на карту и получения результата обработки.
sel_obj — строка, содержащая ID текущего объекта (файла или папки) выбранного командой SELECT
. Эта строка меняется всякий раз, когда команда SELECT выполняется.
atr — здесь мы запоминаем ATR текущей карты, чтобы можно было вывести его на экран, не запрашивая карту каждый раз (такой запрос сбрасывает состояние выбора файла в карте).
card_connection_observer — наблюдатель, который привязывается к каждому connection, подробности ниже.
def __init__(self):
super(APDUShell, self).__init__(completekey=None)
self.reader = select_reader()
self._clear_context()
self.connection = None
self.card_connection_observer = ConsoleCardConnectionObserver()
CardMonitor().addObserver(self)
Мы, помимо инициализации данных, добавляем себя в наблюдателиsmartcard.CardMonitoring.CardMonitor
— объекта, который реагирует на события взаимодействия ридера и карты (карта помещена в ридер, карта извлечена из ридера) и оповещает об этих событиях smartcard.CardMonitoring.CardObserver
, т.е. нас. Данный вид оповещения настраивается только один раз за время жизни нашей оболочки. CardMonitor
является синглтоном, поэтому мы не заботимся о времени жизни его экземпляра.
Также обращаю внимание на экземпляр smartcard.CardConnectionObserver.ConsoleCardConnectionObserver
— это готовый библиотечный объект-наблюдатель, отслеживающий состояние канала общения с картой и печатающий это состояние в консоль. Мы его будем навешивать на каждое новое соединение с картой.
def update(self, observable, handlers):
"""CardObserver interface implementation"""
addedcards, removedcards = handlers
if self.card and self.card in removedcards:
self._clear_connection()
self._clear_context()
for card in addedcards:
if str(card.reader) == str(self.reader):
self.card = card
self._set_up_connection()
break
Это, собственно, поведение smartcard.CardMonitoring.CardObserver
. Если наша текущая карта находится в списке removedcards
, то мы очищаем состояние оболочки для текущей карты.
Если в нашем выбранном ридере (и в списке addedcards
, заодно) появилась новая карта, то мы инициализируем новое состояние оболочки для этой карты.
def default(self, line):
"""Process all APDU"""
if not line or self.card is None:
return
try:
apdu = toBytes(line)
data, sw1, sw2 = self.connection.transmit(apdu)
# if INS is A4 (SELECT) then catch and save FID if select is successful
if apdu[1] != APDUShell.SELECT_COMMAND_INSTRUCTION or sw1 not in APDUShell.SELECT_SUCCESSFUL_SW1:
return
self.sel_obj = toHexString(apdu[5:], PACK)
except (TypeError, CardConnectionException) as e:
try:
print(e.message.decode(locale.getpreferredencoding()))
except AttributeError:
print(e.__class__.__name__ + ' (no message given)')
Здесь все введенные пользователем шестнадцатиричные APDU превращаются в списки байтов и отправляются на карту. Замечу, что единственное, что мы делаем с результатом здесь, это определяем, не является ли отправленная команда успешным SELECT
-ом. Если да, то мы обновляем ID последнего выбранного объекта для печати в приглашении пользователю.
Всю остальную рутинную работу по интерпретации и отображению результата команды для пользователя выполняет наш ConsoleCardConnectionObserver
.
Небольшое попутное отступление
лично мне не очень нравится, какConsoleCardConnectionObserver
отображает результат исполнения APDU — он не отделяет SW от результирующих данных так, как мне этого хотелось бы. Я использовал его только, чтобы не захламлять код примера маловажными деталями. Однако, если кому-то интересно, код методаupdate()
моего наблюдателя есть в этом коммите.
def _set_up_connection(self):
"""Create & configure a new card connection"""
self.connection = self.card.createConnection()
self.connection.addObserver(self.card_connection_observer)
self.connection.connect()
self.atr = toHexString(self.connection.getATR(), PACK)
Трудяга, который помогает нам каждый раз, когда карта в ридере меняется. Он создает соединение с картой, навешивает на него ConsoleCardConnectionObserver
, и запоминает ATR карты (чтобы команда atr могла вывести его на экран).
def _clear_connection(self):
if not self.connection:
return
self.connection.deleteObserver(self.card_connection_observer)
self.connection.disconnect()
self.connection = None
Антипод _set_up_connection()
, «проводит зачистку», когда карта извлечена из ридера.
На данном этапе мы можем запустить нашу командную оболочку и, в зависимости от наличия кард-ридера, получить просто сообщение об ошибке (ридера нет) или увидеть шелл в работе (счастливчики с ридером). При наличии ридера ничто не мешает вставить в него любую смарт-карту и выполнить команду atr — должно сработать, однако, прошу не забывать, что на своих рабочих SIM и банковских картах вы экспериментируете на свой страх и риск.
До встречи во второй части, предполагаю, что там Python-а не будет (почти или совсем), но будут APDU и SW.
При подготовке статьи мне попалась пара проектов, которые используют данную библиотеку:
https://bitbucket.org/benallard/webscard/src
https://github.com/mitshell/card
Может реальные примеры кода окажутся полезными.