31.10.2018

TrustZone: доверенная ОС и ее приложения

Интернет-портал Habr.com, октябрь, 2018 <br>
Статья с упоминанием компании "Аладдин Р.Д."
В прошлых статьях мы рассматривали аппаратное устройство TrustZone и работу механизма Secure Monitor. Сегодня речь пойдет о TEE и ее приложениях. И если прошлый раз были довольно низкоуровневые вещи, сейчас все будет на вполне высоком уровне — на уровне операционной системы.

Что такое TEE

Что же такое TEE? Это доверенная среда исполнения (Trusted Execution Environment), в первую очередь — это среда исполнения программ. Опишем ее в терминах функции и свойств, но не в смысле программирования, а в философском смысле. Например, у поезда дальнего следования, электрички и такси одна самая главная функция – перевозить людей. А вот по свойствам они отличаются, например: поезд возит между городами, электричка — за город, а такси — преимущественно по городу. Поезд и электричка по билетам, такси — нет. И так далее. Функция TEE — доверенно хранить для нас некоторые данные и запускать для нас приложения. Мы хотим передавать TEE команды: запустить такое-то приложение, взять такие-то данные и сделать с ними то и это. При этом код приложения видеть не можем, равно как и данные. Мы будем только получать результат. Взаимодействие с TEE очень похоже на RPC. Эта функция идеально подходит для разной криптографии, например, для электронной подписи: ключи хранятся в TEE, и мы просим TEE подписать переданные данные хранимым в TEE ключом. Мы получаем результат, но доступа к ключу не имеем. У TEE есть ряд свойств, но основные таковы: a) мы доверяем ее реализации, и б) она надежно отделена от основной ОС устройства, защищена, ее сложно нарушить или сломать. Есть и другие свойства, но мы называем ее доверенной ОС именно за это. Свойство б) самое главное — TEE отделена, и ее сложно нарушить, то есть она защищена. Если смотреть на TEE через призму функций и свойств, то становится ясно, что TEE — это даже не совсем про TrustZone. TrustZone – это один из способов отделения TEE от основной (гостевой) ОС.

Варианты реализации TEE

Если главные свойства TEE — что она отделена и ее сложно нарушить, то мы можем придумать разные варианты реализации TEE.
  • Использовать TrustZone — мы получаем разделение TEE и основной ОС в рамках одного ядра процессора.
  • Запустить TEE на отдельном ядре в рамках системы на кристалле и общаться с ней через аппаратный интерфейс. В некоторых специализированных процессорах есть отдельные доверенные ядра для выполнения TEE, но в магазине их не купить, увы. Но можно и взять двухъядерный кристалл, например, Cortex-A+Cortex-M0/M4 и запустить на Cortex-M TEE.
  • Запустить TEE в отдельном чипе и устанавливать с ним защищенное соединение через внешний интерфейс, например, SPI или SMbus. Для защиты коммуникации использовать криптографические методы. Этот метод используется, когда вы устанавливаете соединение с смарт-картой, например, чипованной пластиковой платежной картой. В каком-то смысле в чипе исполняется TEE, ведь по нашей просьбе она очень доверенно делает финансовые транзакции, хранит данные и т. п. Этот же метод используется в TPM (Trusted Platform Module) современной архитектуры PC.
Мы далее будем говорить только о реализации TEE в TrustZone, потому что это очень распространенный вариант реализации TEE. Но многое сказанное будет относиться и к TEE вообще.

TEE как ОС

В прошлых статьях мы все время называли TEE доверенной ОС и говорили, что она во многом похожа на настоящие операционные системы. Не претендуя на общность, скажем, что в основной массе TEE имеют:
  • приложения и процессы: TEE может загружать приложения и выполнять их;
  • разделение памяти процессов и ядра: используется MMU для защиты пространства памяти процессов и для защиты памяти ядра TEE;
  • потоки, взаимодействие процессов;
  • хранение данных.
Можно придумать и более урезанные варианты TEE, например, без динамической загрузки приложений, без взаимодействия процессов, без потоков, но сами приложения, хранение данных и разделение пространства памяти процессов и ядра останутся. Пример урезанного TEE можно наблюдать сейчас в проекте ARM Trusted Firmware-M для нового поколения микроконтроллеров Cortex-M на платформе ARMv8-M. Это урезанная TEE, сейчас там есть поддержка микроконтроллеров на ядрах Cortex-M23 и Cortex-M33. Это flash-based микроконтроллеры, примерно эквивалентные Cortex-M0 и Cortex-M3, но с поддержкой TrustZone. У них мало ОЗУ, программа выполняется преимущественно из Flash, и поэтому в TEE нет динамической загрузки программ. На данный момент TF-M еще и однопоточная.

Программный интерфейс TEE

Для взаимодействия с другими программными компонентами у TEE есть API:
  • TEE предоставляет API для программ через системные вызовы (Supervisor Call, команда SVC);
  • TEE дает API для Normal World через вызовы Secure Monitor (команда SMC).
Через системные вызовы программы сохраняют данные и вызывают функции ОС. Как любая приличная ОС, TEE старается абстрагировать программы от железа в той или иной степени. Например, Linux абстрагирует работу с файлами через вызовы open, read, write, close – все функции stdio в принципе ложатся на системные вызовы ОС. А TEE также позволяет своим приложениям работать с хранимыми данными через вызовы, которые в абстрактном виде сохраняют и загружают объекты (блоки данных) в хранилище. Еще TEE может предоставлять на системном уровне некоторые криптографические функции и т.д. Для TEE есть набор спецификаций GlobalPlatform, они описывают API, требования, сценарии использования и т. п. Основные API TEE для ее программ описываются в «TEE Internal Core API Specification». Там описаны функции хранения данных, криптографические функции, и т. п. А в «TEE Client API» описано, как вызывать приложения из Normal World. Если ваша TEE реализует эти API, написать приложение для нее будет довольно легко. Благодаря одному API реализуется и переносимость программ.

Отличия TEE от обычной ОС

Два главных отличия TEE от Linuх и других знакомых нам ОС общего применения: 1. TEE выполняет действия не по команде пользователя, а по команде из Normal World; 2. TEE в TrustZone не имеет собственного планировщика. В обычной ОС пользователь генерирует некоторый ввод — вводит команды, щелкает мышкой по иконкам, и ОС обрабатывает этот ввод, передает его программам, а программы его обрабатывают. В серверном варианте ввод идет не от пользователя, а от неких клиентов, скорее всего, по сети. Но ОС, тем не менее, действует исходя из внешних входных данных. TEE же не обрабатывает внешние данные и не передает их приложениям. Вместо этого она обрабатывает команды и данные, переданные из Normal World через TEE Client API, и на этом почти все. Получается, что TEE выступает для ОС как некоторая библиотека с интерфейсом RPC, функции которой вызываются. После обработки функций TEE может ничего не делать. Второе отличие вытекает из первого. TEE в TrustZone делит процессорное время с Normal World и вызывается как библиотека. TEE не выделяет под себя процессорное время постоянно, она тратит столько времени, сколько ей нужно для выполнения запроса и потом передает управление в Normal World. А раз так, то она и не должна иметь своего планировщика — ей достаточно планировщика гостевой ОС. Планировщик основной ОС передает управление в TEE косвенно:
  • планировщик ставит на выполнение задачу;
  • задача вызывает системный вызов ядра;
  • системный вызов вызывает TEE, если это нужно;
  • TEE работает столько, сколько необходимо для выполнения запроса и возвращает управление в Normal World.

Приложения TEE

Приложения, работающие в TEE, называются трастлетами — по аналогии с апплетами, которые работают в смарт-картах. Цитата из Википедии:
Applet (англ. applet от application — приложение и -let — уменьшительный суффикс) — это несамостоятельный компонент программного обеспечения, работающий в контексте другого, полновесного приложения, предназначенный для одной узкой задачи и не имеющий ценности в отрыве от базового приложения.
Trustlet — это Trusted Applet. Это программа для TEE, как мы уже выяснили, общается она с TEE через системные вызовы, у нее есть жизненный цикл и т. п. Но все равно название указывает, что это несамостоятельный компонент. Здесь несамостоятельность выражается в том, что трастлет будет выполнять вызовы из Normal World, а потом отключаться вместе с TEE. Если он закрутится в бесконечном цикле, ядро процессора перестанет выполнять функции ОС, и все в конечном счете повиснет. А вот программа для обычной ОС может крутиться в бесконечном цикле и майнить считать какие-то задачи, это совершенно нормально для программы. В этом плане она самостоятельнее трастлета. Трастлет должен иметь какой-то идентификатор, чтобы Normal World мог его называть. Принято давать трастлетам в качестве имени UUID — уникальные идентификаторы.

Жизненный цикл трастлета

Рассмотрим, как происходит запуск трастлета и выполнение команд. Логично было бы загрузить трастлет в память и начать работать, но в GlobalPlatform TEE Client API для запуска трастлета нужно создать контекст и установить сеанса работы с трастлетом. Создание контекста (context) — это установление соединения между программой Normal World и TEE. При этом спецификация GlobalPlatform предполагает, что в устройстве может быть несколько TEE, и на момент создания контекста можно выбрать, к какой TEE обратиться. В GlobalPlatform TEE Client API для этого предусмотрена функция: $$display$$TEEC_Result TEEC_InitializeContext(const char* name, TEEC_Context* context)$$display$$ Эта функция вызывается из приложения Normal World. Здесь name указывает на выбираемую TEE. Eсли мы хотим TEE по умолчанию или уверены, что у нас только одна TEE — подставляем NULL. В context сохраняется созданный контекст. После создания контекста нужно установить сеанс работы с трастлетом. Тут нам пригодится UUID трастлета. Для этого вызывается функция: $$display$$TEEC_Result TEEC_OpenSession( TEEC_Context* context,TEEC_Session* session, const TEEC_UUID* destination, uint32_t connectionMethod, const void* connectionData, TEEC_Operation* operation, uint32_t* returnOrigin) $$display$$ Сеанс эквивалентен работе с экземпляром программы в обычной ОС: в ОС может быть много экземпляров одной программ, и они будут работать независимо. А в TEE есть много сеансов, и по сути это подключения к уникальным экземплярам трастлета в памяти. При этом область кода будет, скорее всего, одна и та же, отображенная через MMU в память разных процессов. А вот область данных будет у каждого процесса своя, позволяя экземплярам работать независимо. Прямо как в Linux. При вызове TEEC_OpenSession контекст «context» и UUID трастлета «destination» передаются как входные данные. Установленный сеанс будет сохранен в «session». Некоторые параметры здесь и далее мы не будем рассматривать, они не так важны для понимания. В момент создания сеанса трастлет может быть загружен в память. Это то же, что происходит с приложениями в операционной системе. В большой TEE за это отвечает линковщик, он загружает бинарный образ трастлета, это такой подписанный ELF-файл. Если это маленькая TEE, трастлет должен быть уже загружен в память — он может быть статически слинкован или, для flash-микроконтроллеров, записан в flash-память по заданному адресу. Давайте предположим, что у нас большая TEE, и нужно загрузить трастлет в память. Откуда он берется? В принципе TEE на момент загрузки нужен объект с неким UUID, и механизм получения этого объекта может быть любой:
  • объект может быть уже в памяти;
  • объект может быть размещен статически в flash-памяти (для flash-микроконтроллеров);
  • объект может быть статически слинкован с TEE – для системных трастлетов;
  • наконец, можно загрузить файл в ОЗУ с файловой системы, или даже по сети.
Спросите себя потом, как же это TEE загружает данные с файловой системы или по сети?!! После загрузки образа трастлета у него проверяется электронная цифровая подпись. Используется система сертификатов, и TEE будет проверять, что трастлет подписан стороной, которой доверяет и TEE. Это очень важно, потому что это исключает возможность загрузки подмененного трастлета с каким-то malware. Когда образ трастлета получен и подпись проверена, TEE создает в MMU пространство адресов для экземпляра трастлета, а линковщик загружает область кода в память, отображает его в адресное пространство трастлета и инициализирует область данных. В результате получается полностью инициализированный экземпляр трастлета для работы с конкретным вызвавшим приложением — это и есть создание сеанса. После того, как сеанс создан, трастлет находится в полной готовности и может выполнять запросы от вызвавшего приложения. Для того чтобы вызывать функции трастлета из ОС, используется функция: $$display$$TEEC_Result TEEC_InvokeCommand( TEEC_Session* session, uint32_t commandID, TEEC_Operation* operation, uint32_t* returnOrigin) $$display$$ Здесь «session» указывает на наш сеанс, то есть на экземпляр TEE и экземпляр трастлета, с которыми мы работаем. «commandID» указывает на вызываемую функцию трастлета. Это именно функция трастлета, а не функция TEE. Вся забота TEE — запустить трастлет и передавать команды, а какие номера commandID назначить для общения с трастлетом — это уже ваше дело, тут никакого правила или глобального списка функций нет. Если нужно передать параметры вызываемой функции, их передают через operation — это указатель на структуру TEEC_Operation. Не будем сейчас сильно углубляться, просто заметим, что эта структура, содержащая до 4 параметров функции (тип TEEC_Parameter). Параметры могут быть простым значением TEEC_Value или указателем на память. У параметров также есть типизация по направлению: TEEC_VALUE_INPUT (входные данные), TEEC_VALUE_OUTPUT (выходные данные), или TEEC_VALUE_INOUT (двунаправленный). Если мы передаем указатель на структуру TEEC_Operation, ее сначала нужно инициализировать: задать все значения и направления. По завершении вызова мы можем проверить возвращенные значения в этой структуре (для TEEC_VALUE_OUTPUT и TEEC_VALUE_INOUT). За время сеанса мы можем вызывать функции трастлета столько раз, сколько нам потребуется. В конце работы нужно будет завершить сеанс и освободить контекст вызовами TEEC_CloseSession и TEEC_FinalizeContext. Все это очень напоминает RPC, не так ли? В принципе, все операции с TEE и задуманы как RPC, и благодаря этому можно работать с самыми разными реализациями TEE: в TrustZone, в отдельном ядре, в отдельном чипе.

Supplicant

Выше мы задались вопросом: как TEE загружает данные с файловой системы или по сети? Если задуматься, TEE сама не имеет доступа к файловой системе ОС. То есть, TEE реализованная в TrustZone, могла бы иметь такой доступ, но тогда ей нужно было бы делить его с Normal World, а это не так-то просто. Например, Linux постоянно работает с файловой системой, и актуальное ее состояние есть только в памяти ядра Linux, а не на диске. Если TEE захочет вмешиваться и работать с файловой системой параллельно, это будет очень непросто. С сетевым обменом то же самое. Кроме того, TEE — довольно маленькая ОС, и реализовывать в ней драйверы низкого уровня для работы с носителями, с сетевым контроллером, поддерживать сетевой стек или драйвер ФС было бы накладно. Кроме того, это многократно увеличивает attack surface — был бы шанс взломать TEE, подсунув необычный inode на ext2 или что-то такое. Мы так не хотим. Поэтому при запуске ОС загружается так называемый Supplicant — программа-помощник. Она все время находится в соединении с TEE, и TEE использует ее для обращения к ресурсам Normal World. Поэтому, если TEE хочет загрузить образ трастлета с файловой системы, она обращается к Supplicant: $$display$$TEE: А подайте-с объект с UUID таким-то? Supplicant: (Загружает объект с файловой системы) Извольте-с! $$display$$ Конечно, такие обращения должны быть проверены на безопасность. В данном случае мы проверяем подпись в трастлете и почти ничем не рискуем — либо подпись верна и трастлет пойдет в работу, либо подпись неверна. То есть рискуем — трастлета может не оказаться, Supplicant может быть не запущен, но это уже другая часть модели угроз.

Библиотека userspace

Программный интерфейс (вызовы TEEC_OpenSession и т. д.) реализуется с помощью библиотеки, которая транслирует вызов с уровня приложения в TEE. При реализации TEE в TrustZone для этого библиотека должна сначала передать вызов на уровень ядра ОС, так как только ядро ОС может вызывать SMC (Secure Monitor Call). В связке Linux + OP-TEE библиотекой userspace является libteec. Она транслирует вызовы GlobalPlatform TEE Client API в драйвер ядра через операции ioctl над файлом устройства: при запуске ОС загружается модуль ядра (драйвер), драйвер создает файл устройства. Открывая файл устройства с помощью libteec, пользовательская программа может работать с TEE Client API. То есть, работает такая конструкция: Приложение > libteec > файл устройства > драйвер ядра > SMC > TEE > трастлет.

Пример работы трастлета

Вот как это работает в реальном применении:
Здесь трастлет используется для электронной подписи документов. Программа из Linux вызывает трастлет, для чего последовательно создается контекст TEE, сеанс с трастлетом, передаются данные для подписи, и возвращается электронная подпись.

Заключение

В этой статье мы разбирались, что такое TEE и трастлеты. Мы познакомились с API TEE и узнали, как вызываются трастлеты. Мы сознательно оставили в стороне многие вещи, такие как использование Shared Memory и написание трастлетов, ведь статья не претендует стать исчерпывающим руководством. Если вы заинтересовались темой TEE, то продолжайте изучать самостоятельно: начать можно с изучения спецификаций GlobalPlatform или с разбора работы OP-TEE. Вы также можете прислать нам резюме с пометкой «TrustZone».