Работаем со смарт-картами, используя Python (часть 1)

    Сначала, на момент задумки, в 2014 году, данная статья планировалась как единая публикация, но, проработав материал (лень вынудила растянуть этот процесс), я понял, что необходимо её разделить на две части:


    1. Знакомство с библиотекой и написание/разбор кода специального командного процессора, который ее использует.
    2. Использование командного процессора из ч.1 для чтения содержимого файла с симки, которую я, однажды, подобрал на улице (никаких персональных данных раскрыто не будет). Узнаем, как отучить Windows встревать в наше взаимодействие с картой, а также, возможно, затронем тему выбора (активации) системного приложения на карте (если моя экспериментальная карта окажется UICC).

    Думаю, для профи-карточников первая часть будет представлять бо́льший интерес, а вторая часть будет интересна, прежде всего, новичкам в этой теме (и будет иметь метку 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, «семерка» и, вероятно, «десятка», с настройками по умолчанию, «ставит палки в колеса» при работе с картой в ридере:


    1. При помещении каждой карты в ридер эта карта считается устройством Plug&Play, для нее системой «ищутся драйверы».
    2. Если мы дождались окончания п. 1, то система начинает искать на карте сертификаты, при этом общаясь с ней своими APDU, эти APDU смешиваются с нашими и возникают коллизии, приводящие к сбоям в наших программах.

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


    Разработка начата под эгидой одного из ведущих (и на момент создания, и сейчас) игроков карточного рынка.
    Поддерживаются обе ветки Python (2 и 3).


    мой опыт использования с версиями Python…

    В рабочем окружении я использую связку pyscard + Python 2.7, но, для статьи, мне показалось правильным задействовать актуальную на сегодня ветку Python (3.6)


    На мой взгляд библиотека pyscard спроектирована не особо pythonic и больше напоминает порт какого-то Java фреймворка, однако полезности её это не уменьшает, по крайней мере для меня, хотя имена модулей выглядят странно, конечно.


    Точкой входа в библиотеку является пакет smartcard.


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


    Установка pyscard возможна следующими способами:


    • с PyPi (pip install pyscard) — подходит для систем, настроенных на сборку артефактов из исходников, используется swig (ок для mac и, возможно, linux)
    • Из бинарного дистрибутива, который можно забрать c appveyor или c sourceforge (великолепно прокатывает в Windows)

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


    1. Посмотрим на типовую структуру/шаблон программы;
    2. Бросим взгляд на, по моему мнению, важнейшие объекты библиотеки, что должно убедить пользоваться не низкоуровневым smartcard.scard, а именно smartcard;
    3. Проиллюстрируем применение библиотеки на реальном примере — напишем командный процессор (шелл) на Python 3.6, где командами будут прямо «APDU в хексе» и ответ с карты будет выводиться в консоль. Также будут поддерживаться текстовые команды exit и atr.

    Типовой шаблон программы


    Пора уже сделать вброс порции кода, а то всё скучные вступительные «бубубу»...


    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)

    Какие задачи решает (практически любая) программа, работающая со смарт-картами в ридере? А вот эти:


    1. Выбор ридера, с которым мы будем взаимодействовать
    2. Определение момента, когда карта окажется в этом ридере
    3. Установка канала связи с картой
    4. Проверка карты на соответствие нашим критериям (мы можем захотеть работать не с каждой картой, которую пользователь нам подсунет)
    5. Обмен данными с картой посредством APDU
    6. Закрытие канала связи с картой
    7. Определение момента, когда карта будет извлечена из ридера

    Замечу, что перечисленные задачи решает, например, прошивка мобильного телефона.


    Важнейшие объекты пакета smartcard


    В этом разделе все имена указаны относительно пакета smartcard.


    Подклассы CardType


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


    Примеры:


    • CardType.ATRCardType (существует в библиотеке) — фильтрация карт по значению ATR. Наше приложение будет реагировать только на карты с определенным значением ATR.
    • USIMCardType (я нафантазировал, можно реализовать) — допустимыми картами являются только USIM, внутри проверяем возможность выбора USIM-приложения.


    CardRequest и его подклассы


    Позволяют свести воедино все требования нашего приложения, касающиеся установления связи с картой:


    • строго задать тип карты (см. выше)
    • ограничить список допустимых ридеров (из уже установленных в системе)
    • изменить таймаут ожидания карты в ридере

    По умолчанию никаких ограничений в CardRequest не ставится.


    CardConnection


    Канал коммуникации нашего приложения с картой, позволяет отправлять на карту APDU и получать ответ, ключевой метод здесь — transmit(). Именно с его помощью происходит непосредственное взаимодействие нашего приложения с картой. Необходимо отметить, что метод transmit() всегда возвращает триплет (кортеж), состоящий из:


    • Ответных данных (содержит реальные данные или None, в зависимости от типа APDU, не все APDU возвращают данные)
    • Первого байта статуса (StatusWord) SW1
    • Второго байта статуса (StatusWord) 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 и ответах карты, которые проходят через наблюдаемое соединение. Пример применения — ведение лога команд и ответов от карты.


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


    Командный процессор с APDU (CLI)


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


    Класс APDUShell


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


    update()


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


    default()


    Код
    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() моего наблюдателя есть в этом коммите.

    _set_up_connection()


    Код
    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 могла вывести его на экран).


    _clear_connection()


    Код
    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
    Может реальные примеры кода окажутся полезными.

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 15
    • 0

      Список APDU команд будет во второй части?
      Для знакомства с тем, что происходит "по ту сторону", — Жикун Чен "Технология Java Card для смарт-карт. Архитектура и руководство программиста".

      • 0

        Во второй части будут только несколько разных APDU. Остальное есть в указанных в начале стандартах.

        • 0
          APDU команды они разные. Хотите конкретных примеров — спрашивайте.
          А для знакомства с содержанием APDU команд/ответов вот например (уж не сочтите за рекламу):
          ISO 7816 APDU parser
          EMV ICC APDU parser
          NFCForum Type 4 Tag Operations

          Для себя остановился на luajit прослойке к Winscard.dll / pcsclite. Слать APDU скрипты более чем достаточно. Криптография, если надо, тоже на прямую к openssl.
          Только хардкор и никаких сторонних питонов / яв.
          • 0
            Да со списком команд проблем нет, терминальная программа и карт-ридер в наличии.
        • 0

          Del

          • 0
            Есть карт-ридер и несколько банковских карт с истёкшим сроком действия (пин-коды известны)
            Возможно ли «заиспользовать» их в своём DIY-проекте?
            Или в банковских картах используются нестандартные команды?
            • 0

              С банковскими картами я незнаком, но предполагаю, что часть команд ISO 7816-4 должна поддерживаться (хотя бы SELECT, GET RESPONSE, READ BINARY, READ RECORD). Команды, думаю, стандартные, но стандарты там свои.
              Схема там, судя по интернетам, обычная для UICC — выбираем приложение по AID и вперед.
              AID для Visa и MC известны.

              • 0

                Без ключей шифрования ничего не сделаете. Да и доступ к файловому менеджеру карты тоже закрыт.

                • 0

                  В смысле, там каждый APDU шифруется и/или подписывается? Понятно, что финансовых операций не совершим, но выбрать/прочитать какие-то файлы можно (если знать об их существовании)? Или там общение происходит "инвелопами", типа как OTA в телекоме?

                  • 0
                    Ключи шифрования — они для транзакций. Да, они тут ни при чём, промахнулся.
                    Есть такой механизм защиты — брандмауэр аплетов, разграничивающий права доступа к общей и защищённой областям памяти (к полям и методам аплета). Файл приложения тоже вряд ли прочитаете.
                • +1

                  Да, возможно. Пин-коды правда можно выкинуть, но считать данные с чипа карты (напр. её номер) и воспользоваться ими ничего не мешает. См. Book 3 – Application Specification. Как шпаргалку можно использовать этот туториал (на javascript).

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

                      2mihmig: Для домашнего использования будет вполне достаточно воспользоваться оффлайн аутентификацией. Разве что при реализации придётся игнорировать срок действия сертификатов на карте.
                  • +1
                    Если под банковскими картами вы подразумеваете Международные Визы/Мастеркарды, то они вписываются в стандарт EMV. Добро пожаловать на сайт EMVCo за стандартами для контактных и бесконтактных карт. ПИН вам не нужен. Почитать данные с карт вы сможете и без ПИНа. Для иденитификации данных получите более чем необходимо. А вот писать на карты и/или менять файловую систему, не имея ключей эмитента — это никак нет.
                  • 0
                    Можно ли использовать смарт-карту для входа в систему Windows без использования AD?
                    Может есть какой сторонний софт?

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