Привет, Гиктаймс!

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

Итак, смарт-карта на основе JavaCard — это карта, на которой приложения исполняются на JavaCard Virtual Machine (ограниченная версия Java Virtual Machine, адаптированная для смарт-карт) в так называемом JavaCard Runtime Environment (который с Java Runtime Environment имеет очень мало общего).

Что касается терминологии, то приложения называются Applets и содержатся в Packages (пакетах). Пакеты распространяются в CAP-files (вместо Jar-files). Пакеты и приложения имеют собственный AID (Application Identifier). Это необходимо для того, чтобы их можно было однозначно идентифицировать в таких командах, как: SELECT, INSTALL, DELETE, и т.д. (SELECT описывается в ISO7816-4, а JavaCard и остальные команды — в Global Platform).

Жизненный цикл Applets несколько отличается от привычного жизненного цикла приложений для компьютеров. Applet — это любой класс, наследующий от базового класса «Applet». При установке приложений вызывается его статический метод install. Этот метод должен создать объект соответствующего класса и вызвать на него метод register. Впоследствии объект будет сохранен в системе и получит собственный AID, который будет использован для дальнейшего общения с приложением. Объект и его поля данных сохраняются в NVM (Non-Volatile Memory). Каждый объект или массив, созданный приложением с помощью оператора «new», также будет находиться в NVM. Это означает, что, в отличие от традиционных компьютерных программ, состояние приложений JavaCard является постоянным и не теряется даже при выключении карты.

Общение с приложением


В любой момент на каждом открытом логическом канале есть одно активное приложение. Активное приложение — это то приложение, которое получает все APDU, посылаемые терминалом. При получении APDU JavaCard вызывает метод process активного приложения, который берет полученный APDU как единственный параметр. Именно этот метод является ядром Applet, поскольку там обрабатываются запросы от терминала. Полученный объект APDU также используется для того, чтобы посылать ответы.

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

  • при reset карты или при открытии логического канала система, как правило, активирует то приложение, которое отмечено как Default Application
  • с помощью команды SELECT с P1 = 0x04 и AID (полный или частичный) приложения в Data

Когда приложение активируется с помощью команды SELECT, то сразу после активизации будет вызван метод process с APDU, содержащим данную команду. Таким образом Applet имеет возможность посылать информацию в ответ на команду SELECT при активизации. Класс Applet предоставляет метод selectingApplet() для того, чтобы определить, если полученная команда вызвала активизацию приложения.

Приложение также имеет возможность переписать методы select() и deselect(), чтобы, соотвественно, провести инициализацию при активизации и де-инициализацию при де-активизации. Эти методы будут вызваны вне зависимости от того, каким образом было активировано приложение.

Логические каналы можно открывать и закрывать с помощью команды MANAGE CHANNEL. По открытым логическим каналам можно посылать любые команды, в том числе SELECT. Именно SELECT и MANAGE CHANNEL являются единственными командами, которые обрабатываются напрямую системой, а не активным приложением. Хотя в случае команды SELECT можно сказать, что она обрабатывается и системой, и активным приложением.

Полные правила активизации приложения и обработки команды SELECT довольно сложные, и существуют маленькие противоречия между JavaCard и Global Platform. Тем не менее, я затрону данную тему в одной из моих следующих статей. Сейчас стоит сказать, что карты зачастую следуют правилам, описанным в Global Platform.

Управление памятью


Как я уже сказал выше объекты и массивы по умолчанию сохраняются в NVM. Помимо NVM, JavaCard также дает возможность создать массивы в RAM с помощью ряда методов из класса JCSystem. При этом существуют 2 вида временной памяти: Clear on Reset и Clear on Deselect. Первая стирается при выключении или reset карты. Вторая стирается тогда, когда Applet перестает быть активным (т.е: при SELECT другого приложения, выключении, reset, и т.д.). Стоит отметить, что, хотя содержание массива стирается (т.е. там пишутся все нули или false), сам массив остается, и его можно, к примеру, сохранять в поле данных объекта.

Что сохраняется в NVM:
  • Все объекты и их поля данных.
  • Все массивы.
  • Содержание массивов, созданных оператором new.

Что сохраняется в RAM (CLEAR_ON_RESET либо CLEAR_ON_DESELECT):
  • Содержание массивов, созданных функциями JCSystem.makeTransient<тип>Array. Важно отметить, что объекты, находящиеся в массиве, созданном функцией JCSystem.makeTransientObjectArray(), все же будут сохраняться в NVM. Только ссылка на них будет в RAM.
  • Global Arrays: системные (APDU буфер и Install Parameters буфер), а также Global Arrays, созданные с помощью JCSystem.makeGlobalArray().
  • Буфер для транзакций. Однако прямой доступ к этому буферу невозможен.


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

1) Желание гарантировать, что при успешной установке приложения, оно не перестанет работать, если какое-то другое приложение взяло себе всю свободную память.
2) Сборщик мусора — необязательная (хоть и очень распространенная) функция. Это означает, что при динамическом создании обьектов существует риск, что созданные объекты никогда не будут удаляться. Со временем это приведет к отсутствию свободной памяти. Даже в случае присутствия сборщика мусора, эта процедура происходит не автоматически, а по запросу приложения (JCSystem.requestObjectDeletion()).

Впоследствии код приложения редко получается «чистым» по параметрам объектно-ориентрованного программирования. Использование общего byte[] для множества различных операций — очень распространенная практика. Классы, состоящие исключительно из статических методов и работающие на byte[], тоже не редкость. К тому же, создание классов и объектов сводится к минимуму. Памяти мало и ее нужно беречь любой ценой.

Стоит также отметить роль объекта APDU. Его буфер, находящийся в RAM, стирается перед получением каждой команды. Его принято использовать не только для формирования ответа, но часто даже как временный буфер. Его размер — как минимум, 256 байтов или намного больше, если карта поддерживает Extended Length.

Applet Firewall


Важной чертой JavaCard, которая может путать программистов, является присутствие так называемого Applet Firewall. Цель Firewall — это не допускать, чтобы какой-либо Applet имел доступ к данным иного. Сразу скажу, что JavaCard допускает общение между приложениями с помощью Shareable Interfaces, а также чтобы пакеты являлись библиотеками классов и функций, которые могут быть использованы в разных приложениях. Именно поэтому возникает необходимость firewall. Основные принципы Applet Firewall следующие:

  • Каждое приложение принадлежит одному контексту. Все приложения из одного пакета принадлежат одному и тому же контексту.
  • Каждый объект принадлежит одному приложению или системе.
  • У пакета без приложений (библиотеки) нет контекста. Объекты из его классов принадлежат приложению, которое их создало.
  • В системе всегда есть один активный контекст.
  • Доступ (чтение/написание в случае массива и вызов методов в случае объекта) разрешается только к объектам, принадлежащим активному контексту.
  • Система имеет доступ ко всем объектам.
  • Вызов статических методов всегда разрешается и исполняется в активном контексте. Доступ к статическим полям данных тоже разрешается. Однако объекты, на которые ссылаются статические поля, следуют общим правилам firewall.
  • Временные массивы из типа CLEAR_ON_DESELECT доступны только тогда, когда активный контекст соответствует контексту активного приложения.

С помощью Shareable Interface программист может позволить определенным методам одного объекта быть доступными приложениям из другого контекста. Когда один из этих методов вызван, то система переключает контекст на тот контекст, которому принадлежит объект, предоставляющий Shareable Interface. Это означает, что данный метод имеет доступ только к объектам, принадлежащим его контексту, и если, к примеру, передать этому методу byte[] как параметр из оригинального контекста, то при доступе произойдет ошибка. После завершения исполнения метода контекст переключается обратно на изначальный.

Приложение также имеет доступ к определенным объектам, принадлежащим системе. Такие объекты называются Entry Points. Существуют два вида Entry Points: временные и постоянные. Постоянные Entry Points — простые, доступ к ним разрешен с любого контекста. Примером этому являются объекты из класса AID. Временные Entry Points, напротив, имеют ограничение, не позволяющее сохранять ссылки на них в полях данных объекта или статических полях класса. Пример временного Entry Point — объект APDU.

Начиная с JavaCard 3.0.4, существует также возможность создавать Global Arrays с помощью метода JCSystem.makeGlobalArray(). Их поведение точно такое же, как и у временных Entry Points. Они преимущественно нужны в качестве параметра для методов, вызванных по технике Shareable Interface.

Атомарность и транзакции


JavaCard гарантирует атомарные операции при изменении полей данных объектов или классов. Атомарность также обеспечена методами, работающими над массивами (кроме тех, которые имеют суффикс NonAtomic в названии). Это означает, что, к примеру, Util.arrayCopy либо скопирует все байты (при успешном исполнении), либо оставит массив неизменным в случае ошибки или потери энергии.

Если необходимо провести изменение в нескольких постоянных полях и массивах в одной атомарной операции, то JavaCard также предоставляет функции JCSystem.beginTransaction() для начала транзакции и JCSystem.commitTransaction() для ее завершения. Все изменения, происходящие в массивах и постоянных полях между вызовами JCSystem.beginTransaction() и JCSystem.commitTransaction(), автоматически будут являться частью транзакции. Если транзакция отменяется из-за ошибки, потери энергии или вызова метода JCSystem.abortTransaction(), то система восстановит изначальное состояние. Стоит запомнить, что техническая реализация транзакции использует дополнительный системный буфер. Если буфер заполняется полностью, то система дает ошибку TransactionException.

RMI


JavaCard поддерживает технологию RMI (Remote Metod Invocation). В этой статье я не буду освящать данную технологию. Скажу лишь только, что данная функциональность не является распространенной, и многие карты не поддерживают ее.

API


Большая часть JavaCard API посвящена криптографии. Есть классы для шифрования, создания и проверки цифровой подписи, генерирования ключей и случайных чисел, расчета контрольных сумм и хешов, а также для реализации схем обмена ключами. Помимо криптографии JavaCard также предоставляет классы для сохранения и проверки PIN и даже биометрических данных. Остальные классы предоставляют доступ к функциональности, описанной в других главах или являются вспомогательными для работы со строковыми, номерами, TLV и т.д. Что касается API, то он будет трактоваться в другой серии статей.

Основные ограничения JavaCard по сравнению с Java


Язык программирования приложений JavaCard — это Java. Почти Java. Так, типы char, float, double, long и enum не поддерживаются. Тип int является необязательным для карт (обычно современные карты его поддерживают) и его использование, если его не активировать соответствующей опцией, приведет к ошибке при конвертации приложения в CAP-file. Забудьте о for, итератор и lambda. Generics, статические import и аннотации (только Runtime Invisible) поддерживаются конвертером, начиная с версии 3.0.0, т.к. они не влияют на исполнение кода.

Для компилирования кода используется обыкновенный компилятор от JDK. Ошибки несовместимости будут заметны только при конвертации в CAP-file или при использовании умного IDE для JavaCard.

Самая большая проблема для программистов, на самом деле, — отсутствие int по умолчанию. Обычно они, действительно, не нужны, поэтому не хочется их активировать без надобности. Однако компилятор Java имеет привычку автоматически приводить результат всех арифметических операций к типу int. Для избежания этого в коде приходится использовать явное приведение типа к short или byte, что делает код менее читаемым.

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

Заключение


Итак это было мое введение в JavaCard. Если вы считаете эту статью интересной или, по крайне мере, она вызвала у вас интерес к данной теме, то вы можете написать в комментариях, о чем конкретно вы хотели бы узнать побольше. Так я смогу решить в каком порядке и насколько подробно написать о той или иной функциональности и применении JavaCard. В любом случае следующая статья, посвященная Global Platform, будет завершающей эту серию.

Комментарии (8)


  1. Disasm
    18.06.2015 01:23

    Когда-то разбирался с устройством виртуальной машины JavaCard, и так и не понял, как она решает, какие данные хранить в RAM, а такие в NVM. Ещё не очень понятно, могут ли данные из RAM перекочевать в NVM и наоборот. Не могли бы вы прояснить ситуацию?


    1. Valle
      18.06.2015 09:11

      Очень просто — все что создается оператором new — идет в NVM и это возможно только в конструкторе объекта, т.к. это по факту инсталляция. А RAM в нормальном понимании нет, точнее есть, но доступ к ней через библиотечные функции, см. www.win.tue.nl/pinpasjc/docs/apis/jc222/javacard/framework/JCSystem.html


    1. brainnolo Автор
      18.06.2015 09:17

      Спасибо за вопрос… сегодня вечером дополню статью и напишу поподробнее о памяти, и о том, какие данные хранить в RAM, а какие в NVM.


      1. Disasm
        18.06.2015 12:13

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


        1. brainnolo Автор
          18.06.2015 17:44

          Я дополнил статью, читайте главу «Управление памятью». По поводу второго вопроса: в принципе в спецификациях JCRE описаны все обязательные проверки. Как именно происходит установка, с точки зрения того, какие команды надо послать, описано в Global Platform.


          1. Disasm
            18.06.2015 17:56

            Спасибо. Теперь, вроде бы, понял.

            > По поводу второго вопроса: в принципе в спецификациях JCRE описаны все обязательные проверки. Как именно происходит установка, с точки зрения того, какие команды надо послать, описано в Global Platform.
            Что-то по поводу проверок там всё только в общих чертах. Интересует скорее, как формат файла упрощает выполнение этих проверок. И вообще кому и как он упрощает жизнь.
            С этих же позиций интересует и процесс установки, т.е. как именно работает апплет-установщик, какие действия выполняет, как хранит данные апплета.


            1. brainnolo Автор
              18.06.2015 19:05

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


  1. mayorovp
    18.06.2015 09:14

    Однако компилятор Java имеет привычку автоматически приводить результат всех арифметических операций к типу int. Для избежания этого в коде приходится использовать явное приведение типа к short или byte, что делает код менее читаемым.
    Шел 2015й год…