● Библиотека bootutil (boot/bootutil) ● Загрузочное приложение, boot application (каждый порт имеет свой собственный код в boot/).
Библиотека bootutil выполняет основные функции загрузчика. В частности, имеется отсутствующая часть, относящаяся к финальному шагу перехода в основной образ кода (приложение). Этот последний шаг реализован в загрузочном приложении (boot application). Таким способом функционал загрузчика вынесен в отдельный код, что позволяет реализовать unit-тестирование загрузчика. Библиотека может быть проверена unit-тестом, но не приложение. Поэтому по мере возможности связанный с загрузкой функционал делегирован библиотеке bootutil.
Ограничения. В настоящий момент загрузчик поддерживает только образы со следующими характеристиками:
● Собранные для работы из flash. ● Собранные для работы по фиксированному адресу (т. е. не произвольно перемещаемые).
Формат образа. Следующие определения описывают формат образа.
#define IMAGE_TLV_DEPENDENCY 0x40 /* Образ зависит от другого образа */
#define IMAGE_TLV_SEC_CNT 0x50 /* Счетчик безопасности */
Опциональные записи тип-длина-значение (type-length-value, TLV) содержат метаданные образа, размещенные в конце образа.
Поле ih_protect_tlv_size показывает длину защищенной области TLV. Если защищенные записи TLV присутствуют, то должен присутствовать заголовок информации TLV с magic, равной IMAGE_TLV_PROT_INFO_MAGIC, и защищенные TLV (плюс сам заголовок информации) должны быть включены в вычисление хеша. Иначе хеш вычисляется только по заголовку образа и самому образу. В этом случае значение поля ih_protect_tlv_size field равно 0.
Поле ih_hdr_size field показывает длину заголовка, и таким образом соответствует смещению самого образа. Это поле предоставлено для обратной совместимости в случае изменения формата заголовка образа.
[Карта памяти flash]
Память flash на устройстве разбита на части в соответствии с определенной разметкой (flash map). На верхнем уровне карта flash-памяти отображает числовые идентификаторы ID на области flash. Область flash это регион диска со следующими параметрами:
● Область может быть стерта без влияния на другие области. ● Запись одной области не ограничивает записи в другие области.
Загрузчик использует следующие идентификаторы областей (flash area ID):
/* Не зависят от загрузчки нескольких образов */
#define FLASH_AREA_BOOTLOADER 0
#define FLASH_AREA_IMAGE_SCRATCH 3
/* Если загрузчик работает с первым образом */
#define FLASH_AREA_IMAGE_PRIMARY 1
#define FLASH_AREA_IMAGE_SECONDARY 2
/* Если загрузчик работает с вторым образом */
#define FLASH_AREA_IMAGE_PRIMARY 5
#define FLASH_AREA_IMAGE_SECONDARY 6
Область загрузчика содержит код самого загрузчика. Другие области описывают последующие секции. Память flash может содержать несколько исполняемых образов, поэтому идентификаторы flash area ID первичной (primary) и вторичной (secondary) областей отображаются на основе номера активного образа (с которым в настоящий момент работает загрузчик).
[Слоты образа]
Часть flash-памяти может быть поделена на несколько областей образа, в каждой из них содержатся два слота образа (image slot): первичный слот (primary slot) и вторичный слот (secondary slot). Обычно загрузчик будет запускать код из первичного слота образа, так что образы должны быть собраны таким способом, чтобы они могли запускаться по фиксированному расположению во flash (исключение из этого правила - режим direct-xip и ram-load апгрейд). Если загрузчику надо запустить образ, находящийся во вторичном слоте, то он должен предварительно выполнить копию его содержимого в первичный слот: либо поменять эти образы в слотах местами, либо выполнить перезапись содежимого первичного слота. Загрузчик поддерживает два варианта обновления - либо на основе перестановки слотов (swap-based image upgrade), либо на основе перезаписи первичного слота образа (overwrite-based image upgrade). Однако выбор одной из этих двух стратегий обновления должен быть выполнен и сконфигурирован во время сборки.
Когда используется алгоритм перестановки слотов с помощью временной области flash (swap-using-scratch algorithm), в дополнение к областям слотов образа необходимо также определить во flash специальную scratch-область. Она обеспечивает надежную перестановку образов. У области scratch размер должен быть достаточен, чтобы уместить самый большой сектор из переставляемых слотов. Многие устройства оборудованы flash секторами одинакового небольшого размера, например 4K, однако некоторые могут содержать большие секторы размером 128K или 256K, так что область scratch должна быть размером не меньше, чтобы их уместить. Область scratch используется только когда переключается код firmware (перестанавливаются слоты), т. е. когда выполняется апгрейд прошивки. С учетом этого основная причина выбора самого большого размера для scratch - соблюдения более равномерного износа ячеек flash (flash wear, сектора flash имеют ограниченное количество циклов стирание-запись). Например, scratch-область из 2 секторов будет перезаписана в 2 раза реже, чем scratch-область из 1 сектора. Чтобы оценить идеальный размер scratch для вашего случая учитывают параметры:
● Соотношение размера образа и размера scratch. ● Количество циклов стирания, поддерживаемое аппаратурой flash.
Используется размер образа (вместо размера слота), поскольку копируются только сектора слота, которые фактически используются для хранения копируемого образа. Соотношение размеров image/scratch определяет, сколько раз scratch будет стерта при каждом апгрейде. Количество циклов стирания, поделенное на коэффициент image/scratch даст в результате возможное количество обновлений ПО устройства, которое можно выполнить без риска превысить спицификации flash-памяти.
Для примера предположим, что память устройства допускает 10000 циклов стирания, размер образа 150K, и размер scratch 4K (обычный размер сектора для устройств). Получится следующий результат:
10000 / (150 / 4), примерно до 267 обновлений.
Если увеличить scratch до 16K, то получится:
10000 / (150 / 16), примерно до 1067 обновлений.
"Наилучшего" соотношения не существует, и правильный размер зависит от контекста использования, и это всегда компромисс. Факторы, которые следует учесть: количесво перепрошивок как "в поле", так и во время разработки, а также желаемый уровень надежности в зависимости от указанного производителем допустимого количества стираний. Как правило допустимым считается сотни и тысячи обновлений по месту (у потребителя).
Алгоритм перестановки с использованием scratch подразумевает, области слотов первичного и вторичного образа имеют одинаковый размер. Максимальный размер образа, доступный для приложения, будет следующий:
Это альтернативный алгоритм, не использующий scratch-область. Для перестановки он использует дополнительный сектор в primary-слоте. Такой алгоритм работает следующим образом:
1. Перемещает все секторы primary-слота по одному, начиная с N=0: 2. Копирует N-й сектор из secondary слота в N-й сектор primary слота. 3. Копирует (N+1)-й сектор из primary-слота в N-й сектор secondary слота. 4. Шаги 2 и 3 повторяются, пока не будут поменены местами все сектора слотов.
Этот алгоритм разработан таким образом, что сектор с самым большим номером primary-слота используется только для обеспечения возможности перемещения всех секторов вверх. Таким образом самым эффективным с точки зрения затрат памяти расположением слотов будет случай, когда primary-слот точно на один сектор больше secondary-слота, хотя слоты одинакового размера также допустимы. Этот алгоритм ограничен в применении только для ситуаций, когда все сектора имеют одинаковый размер. Размер всех секторов всех слотов должен быть одинаковым.
При использовании этого алгоритма максимальный размер образа, доступный для приложения:
Здесь N это количество секторов primary-слота, image-trailer-sectors-size это размер трейлера образа, округленный ввех до общего размера занимаемых секторов. Например, если image-trailer-size равен 1056 байт, и размер сектора 1024 байт, то image-trailer-sectors-size будет равен 2048 байт.
Этот алгоритм использует 2 цикла стирания на primary слоте и 1 цикл стирания на secondary слоте для каждой перестановки. Если предположить, что получение нового образа приложением DFU требует 1 цикл стирания на secondary слоте, это должно привести к выравниванию износа flash между слотами.
Этот алгоритм разрешается опцией MCUBOOT_SWAP_USING_MOVE.
Когда разрешен режим direct-xip, флаг активного образа "перемещается" между слотами во время апгрейда, и в отличие от двух предыдущих алгоритмов, загрузчик может запустить образ непосредственно либо из первичного, либо из вторичного слота (без необходимости физического перемещения/копирования данных образа в первичный слот). Поэтому клиент обновления образа, который загружает новые образы, должен знать, какой слот содержит активный образ, и который действует как отложенный образ, и клиент обновления отвечает за корректную загрузку соответствующих образов в соответствующий слот. Во время загрузки (boot time) код загрузчика сначала находит образы в слотах, и затем проверяет номера версий в заголовках образов. Он выберет самый новый образ (у которой номер версии больше), и затем проверит его корректность (проверка целостности, проверка подписи и т. п.). Если образ недопустимый, то MCUboot сотрет его область памяти, и начнет проверку другого образа. После успешной проверки образа запустит его.
Также поддерживается дополнительный механизм "отката" (см. описание далее). Обработка primary и secondary слотов как равноправных имеет свои недостатки. Поскольку образы не перемещаются между слотами, то не поддерживается шифрование/дешифровка на лету (это относится тольк к хранению образа во внешней flash устройства, перенос зашифрованных данных образа все еще возможен).
Стратегии с перезаписью и direct-xip существенно проще реализации по сравнению со стратегией перестановки образов, поскольку загрузчик должен обеспечивать исключение "окирпичивания" устройства даже в случае неожиданного сброса посередине процесса обновления с перестановкой образов. По этой причине остальная часть этого документа описывает поведение загрузчика с перестановкой образов при апгрейде.
В режиме RAM-load слоты эквивалентны. Так же, как и в режиме direct-xip, этот режим также выбирает самый новый образ для загрузки путем чтения и сравнения номеров версий в заголовке образов. Однако вместо того, чтобы запускать выбранный образ "по месту", он копируется в RAM для выполнения. Адрес загрузки в ОЗУ (RAM), куда копируется образ, сохранен в заголовке образа. Режим ram-load может быть полезен, когда в SoC/MCU отсутствует внутренняя flash, однако размера внутреннего RAM достаточно для размещения образов. Обычно в этом случае энергонезависимое хранилище образов реализуется во внешней микросхеме flash или устройстве хранения. Выполненение из внешнего хранилища имеет некоторые недостатки (снижается скорость выполнения, образ подвержен атакам взлома), поэтому образ всегда копируется во внутреннее RAM перед аутентификацией и выполнением. Режим RAM-load требует, чтобы образ был сборан для выполнения из адресного пространства RAM вместо диапазона адресов устройства хранения. Если режим RAM-load разрешен, то платформа должна определить следующие параметры:
#define IMAGE_EXECUTABLE_RAM_START area_base_addr // базовый адрес области
#define IMAGE_EXECUTABLE_RAM_SIZE area_size_in_bytes // размер области в байтах
При использовании загрузки нескольких образов в несколько регионов RAM платформа должна вместо этого определить флаг MULTIPLE_EXECUTABLE_RAM_REGIONS, и реализовать следующую функцию:
Когда разрешен режим ram-load, должна также использоваться опция --load-addr < addr> скрипта imgtool, когда подписываются образы. Эта опция установит флаг RAM_LOAD в заголовке образа, который показывает, что образ должен быть загружен в RAM, и также установит в заголовке образа адрес загрузки.
Когда разрешена опция шифрования (MCUBOOT_ENC_IMAGES) вместе с режимом ram-load, образ проверяется на предмет шифрования. Если образ не зашифрован, то загрузка в RAM происходит так же, как описано выше. Если образ зашифрован, то он копируется в RAM по предоставленному адресу, и затем расшифровывается. На финальной стадии расшифрованный образ аутентифицируется и запускается в RAM.
[Типы boot swap]
При первой загрузке устройства в нормальных услоловиях каждый primary слот несет в себе актуальный образ firmware, который MCUboot может проверить и загрузить. В этом случае перестановка образов (image swap) не требуется. Однако при апгрейде устройства, новый кандидат образа image (образов) представлен в secondary слоте (слотах), который MCUboot должен переставить в primary слот (слоты) перед выполнением загрузки, как описано выше.
Апгрейд старого образа на новый может быть двухшаговым процессом. В этом процессе MCUboot выполняет "тестовую" замену данных образа во flash и загружает новый образ, либо он будет выполнен во время функционирования. Затем новый образ может обновить содержимое flash-памяти во время своего выполнения (runtime), чтобы пометить самого себя "OK", и затем MCUboot будет выбирать его при следующей загрузке. Когда такое происходит, перестановка становится "перманентной". Если это не произошло, то MCUboot будет выполнять откат перестановки ("revert" swap) при следующей загрузке путем перестановки образа (образов) обратно в свое место (места), и попытки загрузить старый образ (образы).
В зависимости от случая использования, первый swap может также быть непосредственно сделан перманентным. В этом случае MCUboot не будет пытаться делать откат образов при следующем сбросе.
Проверка swap-ов поддерживается с целью предоставить механизм отката, чтобы предотвратить ситуацию с "окирпичиванием" устройства, когда было загружено "плохое" firmware. Если устройство терпит сбой сразу после загрузки нового (плохого) образа, то MCUboot вернет обратно старый (рабочий) образ при следующем сбросе устройства вместо того, чтобы снова запускать плохой образ. Это позволяет firmware устройства проверять swap и делать его перманентным только после успешной самопроверки.
Во время startup код MCUboot инспектирует содержимое flash для принятия решения, какой из этих типов "swap" следует выполнить. Затем принятое решение определяет, какой будет последующий процесс.
Возможные типы swap и их смысл:
BOOT_SWAP_TYPE_NONE: "обычный" случай, или случай "no upgrade" (без обновления); делается попытка запустить содержимое primary-слота.
BOOT_SWAP_TYPE_TEST: загрузит содержимое secondary-слота путем перестановки образов. Если перестановка не сделана перманентной, выполнит обратный отка при следующей загрузке.
BOOT_SWAP_TYPE_PERM: перманентно сразу переставит образы, и выполнит загрузку и обновленного образа firmware.
BOOT_SWAP_TYPE_REVERT: предыдущая проверка перестановки (test swap) не сделана перестановку перманентной; переключит обратно старый образ, данные которого находятся теперь в secondary-слоте. Если старый образ пометил себя "OK" при своей загрузке, то следующая загрузка получит swap-тип BOOT_SWAP_TYPE_NONE.
BOOT_SWAP_TYPE_FAIL: swap был неудачным, потому что образ для запуска недопустимый.
BOOT_SWAP_TYPE_PANIC: swap столкнулся с невосстановимой ошибкой.
Тип swap является высокоуровнемым представлением результата загрузки. В последующих секциях описывается, как MCUboot определяет тип swap по битам флагов содержимого flash.
Механизм отката в режиме direct-xip. Режим direct-xip также поддерживает механизм отката (revert), который эквивалентен откату с перестановкой образов обратно. Когда выбран режим direct-xip, то revert может быть разрешен опцией конфигурации MCUBOOT_DIRECT_XIP_REVERT, и также должен быть добавлен трейлер образа в подписанные образы (должна использоваться опция –pad для скрипта imgtool). Для дополнительной информации см. далее раздел "Image Trailer" и документацию imgtool [2]. Также поддерживается напосредственная пометка перманентности образов (корректность образов подтверждается сразу, без необходимости последующей установки перманентности при самопроверке образа), точно так же, как и в swap-режиме. Отдельные шаги revert-механизма режима direct-xip следующие:
1. Выбирается слот, который хранит самый новый потенциальный для загрузки образ.
2. Проверка: был ли этот образ ранее выбран для запуска (во время предыдущей загрузки)?
Да, был выбран: была ли предыдущая самопроверка успешной? Да, самопроверка была успешной: Переход к шагу 3. Нет, самопроверка была неудачной: - Стирание содержимого слота, чтобы он больше не выбирался следующей загрузкой. - Возврат к шагу 1 (попытка выбрать другой, более старый слот, если слоты еще остались). Нет, самопроверка была неуспешная: Пометка образа как "выбранного" (установка в трейлере флага ) copy_done. Переход к шагу 3.
3. Выполнение проверки образа ...
[Image Trailer]
Чтобы загрузчик мог определить текущее состояние, и какие действия надо предпринять во время текущей операции загрузки, используются метаданные, сохраненные в областьях образа flash. Во время процесса swap некоторые из этих метаданных временно копируются в область scratch и из неё.
Эти метаданные находятся в конце областей образа flash, и называются трейлером образа (image trailer). У трейлера образа структура следующая:
Смещение, следующее сразу за этой записью, это начало следующей области flash.
Примечание: минимальный записываемый размер "min-write-size" это свойство аппаратуры flash (часто соответствует размеру страницы или сектора). Если аппаратура поддеживает запись отдельных байт по произвольным адресам, min-write-size равно 1. Если аппаратура позволяет записывать только по четным адресам, то min-write-size равно 2, и так далее.
Трейлер образа содержит следующие поля:
1. Swap status: серия записей, в которых записан прогресс image swap. Чтобы переставить образы полностью, данные перестанавливаются между двумя областями образов по одному сектору, примерно так:
- данные сектора из primary-слота копируются в область scratch, затем этот сектор primary-слота стирается; - данные сектора из secondary-слота копирутся в primary-слот, затем этот сектор secondary-слота стирается; - данные сектора из области scratch копируются в стертый сектор secondary-слота.
В процессе перестановки образов загрузчик обновляет поле swap status, чтобы можно было впоследствии определить, на каком месте сейчас находится перестановка. Таким образом, поле swap status может использоваться для возобновления операции перестановки, если загрузчик остановился до окончания всего процесса и последовал сброс. Значение BOOT_MAX_IMG_SECTORS это конфигурируемое максимальное количество секторов, которое MCUboot поддерживает для каждого образа; его значение по умолчанию 128, однако можно либо уменьшить этот значение, чтобы ограничить расход RAM, либо увеличить для устройств, у которых очень много секторов flash, либо каждый сектор очень маленький, и это требует более сложную конфигурацию, чтобы обработать все секторы слота. Коэффициент min-write-size введен для учета поведения аппаратуры flash.
2. Ключи шифрования (key-encrypting keys, KEK). Эти ключи нужны для шифрования и дешифровки образа (для дополнительной информации см. [4].
3. Swap size: когда начинается новая операция swap, это поле показывает общий размер данных, которые должны быть переставлены (на основе слота с самым большим образом + записии TLV), сюда записывается оставшееся количество, чтобы упростить восстановление из ситуации, когда произошел сброс во время процесса swap.
4. Swap info: 1 байт, который кодирует следующую информацию:
- Swap type: тип операции перестановки, сохраняется в битах 0-3. Показывает тип выполняющейся операции swap. Когда MCUboot возобновляет прерванный swap, он использует это поле, чтобы определить тип операции для выполнения. Это поле содежит одно из значений таблицы ниже. - Image number: номер образа, сохраняется в битах 4-7. Для загрузки одного образа (single image boot) это значение всегда 0. В случае multi image boot показывает, какой из образов перестанавливается, когда процесс был прерван. Одна и та же область scratch используется для все swap-операций образов. Поэтому это поле используется для определения, какому образу приналдежит трейлер, если в области scratch найден статус загрузки, когда возобновляется swap-операция.
Имя
Значение
BOOT_SWAP_TYPE_TEST
2
BOOT_SWAP_TYPE_PERM
3
BOOT_SWAP_TYPE_REVERT
4
5. Copy done: 1 байт, показывающий завершение копирования образа в этом слоте (0x01=завершено; 0xff=не завершено).
6. Image OK: 1 байт, показывающий, подтвержден ли пользователем образ как "хороший" (0x01=подтвержден; 0xff=не подтвержден).
7. MAGIC: поле из 16 байт, идентифицирующий разметку трейлера образа. Может принимать различные значения, в зависимости от максимального поддерживаемого выравнивания образа при записи (write alignment, BOOT_MAX_ALIGN), как определено следующей конструкцией:
В случае, когда BOOT_MAX_ALIGN определено на любое другое значение, не равное 8, максимальное поддерживаемое значение выравнивания закодировано в поле MAGIC, определенное фиксированным 14-байтовым шаблоном:
Замечание: следует иметь в виду, что трейлеры образа делают конечную область слота образа недоступной для размещения в ней данных образа. В частности, размер swap status может быть огромным. Например, для слота из 128 секторов с выравниванием на 4 байта он стал бы 1536 байт.
При startup загрузчик определяет тип boot swap, инспектируя трейлеры образа. При использовании термина "трейлеры образа" подразумевается совокупная информация, предоставляемая обоими трейлерами слотов образа.
Новые swap-ы (без возобновления). Для нового swaps код MCUboot должен инспектировать набор полей, чтобы определить тип выполняемой swap-операции.
Записи трейлеров образа структурируются вокруг ограничений, накладываемых аппаратурой flash. Как следствие у них нет интуитивно-понятного дизайа, и трудно получить представление о состоянии устроства, просто посмотрев на трейлеры образов. Лучше всего сопоставить все возможные состояния трейлера со swap-типами, описанными выше. Ниже воспроизводятся соответствующие swap-типам таблицы.
Замечание: важное замечание по поводу этих таблиц заключается в том, что они должны оцениваться в представленном ниже порядке. Меньшие номера состояний должны получать больший приоритет при тестировании трейлеров образа.
State I
| primary slot | secondary slot |
-----------------+--------------+----------------|
magic | Any | Good |
image-ok | Any | Unset |
copy-done | Any | Any |
-----------------+--------------+----------------'
результат: BOOT_SWAP_TYPE_TEST |
-------------------------------------------------'
State II
| primary slot | secondary slot |
-----------------+--------------+----------------|
magic | Any | Good |
image-ok | Any | 0x01 |
copy-done | Any | Any |
-----------------+--------------+----------------'
результат: BOOT_SWAP_TYPE_PERM |
-------------------------------------------------'
State III
| primary slot | secondary slot |
-----------------+--------------+----------------|
magic | Good | Unset |
image-ok | 0xff | Any |
copy-done | 0x01 | Any |
-----------------+--------------+----------------'
результат: BOOT_SWAP_TYPE_REVERT |
-------------------------------------------------'
Любое из этих трех представленных выше состояний приводит к тому, что MCUboot попытается переставить образы.
Иначе MCUboot не попытается переставить образы, что иллюстрируется State IV.
State IV
| primary slot | secondary slot |
-----------------+--------------+----------------|
magic | Any | Any |
image-ok | Any | Any |
copy-done | Any | Any |
-----------------+--------------+----------------'
результат: BOOT_SWAP_TYPE_NONE, |
BOOT_SWAP_TYPE_FAIL, или |
BOOT_SWAP_TYPE_PANIC |
-------------------------------------------------'
В State IV, когда не было ошибок, MCUboot попытается напрямую загрузить содержимое primary-слота, и результат будет BOOT_SWAP_TYPE_NONE. Если образ в primary-слоте недопустимый, то результат будет BOOT_SWAP_TYPE_FAIL. Если во время загрузки произошла фатальная ошибка, то результатом будет BOOT_SWAP_TYPE_PANIC. Если результат BOOT_SWAP_TYPE_FAIL или BOOT_SWAP_TYPE_PANIC, то MCUboot зависает вместо того, чтобы загружать неправильный или скомпроментированный образ.
Замечание: важным предостережением к вышесказанному является результат, когда запрашивается swap и проверка образа во secundary слоте завершилась неудачей из-за ошибки хеша или подписи. Это состояние ведет себя как State IV с дополнительным действием маркировки образа в primary слоте как "OK", чтобы предотвратить дальнейшие попытки swap.
Возобновленный swap. Если MCUboot определил, что это восстановление прерванного swap (например, произошел неожиданный сброс посередине процесса), то он полностью определяет операцию для возобновления путем чтения поля swap info из активного трейлера и распаковки swap type из бит 0-3. Набор таблиц в предыдущей секции для случая возобновления не нужен.
[Функционирование на верхнем уровне]
Теперь с определенными выше терминами можно рассмотреть, как работает загрузчик. Сначала представим обзор процесса загрузки на верхнем уровне, последующие секции более подробно будут описывать каждый шаг этого процесса.
Процедура:
1. Проверяется регион swap status: есть ли сейчас состояние прерванного swap, который должен быть продолжен? Да: завершение начатого swap; переход на шаг 3. Нет: переход на шаг 2.
2. Проверяеются трейлеры образов: был ли запрошен swap? Да: запрошенный образ допустимый (прошел проверку на целостность и безопасность)? Да: a) Выполнить операцию swap. b) Сохранить статус завершения процедуры swap в трейлеры образов. c) Перейти к шагу 3. Нет: a) Стереть неправильный образ. b) Сохранить статус отказа процедуры swap в трейлеры образов. c) Перейти к шагу 3. Нет: перейти к шагу 3.
3. Загрузиться в образ из primary-слота.
Multiple image boot. Когда flash содержит несколько исполняемых образов, работа загрузчика несколько усложняется, однако осуществляется подобным образом, как в ранее описанной процедуре с одним образом. Каждый образ может быть обновлен независимо, поэтому flash дополнительно делится на разделы для размещения двух слотов у каждого образа.
+--------------------+
| MCUboot |
+--------------------+
~~~~~ < - эта область памяти не обязательно должна быть смежной
+--------------------+
| Image 0 |
| primary slot |
+--------------------+
| Image 0 |
| secondary slot |
+--------------------+
~~~~~ < - эта область памяти не обязательно должна быть смежной
+--------------------+
| Image N |
| primary slot |
+--------------------+
| Image N |
| secondary slot |
+--------------------+
| Scratch |
+--------------------+
MCUboot также может обрабатывать зависимости между образами. Например, если образ нужно откатить, то может понадобиться откатить также и другой образ (например, чтобы обеспечить совместимость API), или может быть просто запрещено обновление из-за неразрешенной зависимости. Поэтому все прерванные swap-ы должны быть завершены, и все типы swap должны быть определены для каждого образа до проверки зависимостей. Обработка зависимостей более подробно описана в следующей секции. Процедура multiple image boot procedure организована в циклах, которые проходят по всем образам firmware. Ниже показан высокоуровневый обзор процесса загрузки.
• Цикл 1. Итерации по всем образам.
1. Проверяется область swap status текущего образа: возобновляется ли прерваный swap? Да: проверка допустимости определенных типов swap других образов. Завершение операции частичного swap. Пометка типа swap как None. Переход к следущему образу. Нет: переход к шагу 2.
2. Проверка трейлеров образа в primary и secondary слотах: был ли запрошен swap? Да: проверка допустимости ранее определенных типов swap других образов. Запрошенный образ допустимый (по проверке целостности и безопасности)? Да: установка ранее определенного типа swap для текущего образа. Переход к следующему образу. Нет: стирание недопустимого образа. Сохранение ошибки процедуры swap в трейлеры образа. Пометка типа swap как Fail. Переход к следующему образу. Нет: пометка типа swap как None. Переход к следующему образу.
• Цикл 2. Итерации по всем образам.
1. Текуший образ зависит от другого образа (образов)? Да: все зависимости образа удовлетворены? Да: переход к следующему образу. Нет: изменение типа swap в зависимости от того, какой был предыдущий тип. Перезапуск проверки зависимости, начиная с первого образа. Нет: переход к следующему образу.
• Цикл 3. Итерации по всем образам.
1. Был ли запрошен swap образа? Да: выполнить операцию обновления образа. Сохранение завершения процедуры swap в трейлеры образа. Переход к следующему образу. Нет: переход к следующему образу.
• Цикл 4. Итерации по всем образам.
1. Проверка образа в primary-слоте (на целостность и безопасность), или как минимум базовая проверка работоспособности, чтобы избежать загрузки в пустую область flash.
• Загрузка образа в primary-слоте на 0-й позиции образа (образ в цепочке загрузки запущен из другого образа).
Multiple image boot для загрузки RAM и direct-xip. Работа загрузчика отличается при выборе стратегии ram-load или direct-xip. Карта flash подобна стратегии swap, однако здесь не нужна область Scratch.
• Цикл 1. Итерация по всем образам, пока они все не будут загружены, и пока не будут удовлетворены все зависимости.
1. Субцикл 1. Итерация по всем образам. Содержится ли в любом слоте образ? Да: выбор самого нового образа. Копирование его в RAM при стратегии ram-load. Проверка образа (на целостность и безопасность). Если проверка была неуспешной, то удаление образа из flash, и попытка проверить другой слот (образ должен быть удален также и из RAM в случае стратегии ram-load). Нет: возврат с ошибкой.
2. Субцикл 2. Итерация по всем образам. Зависит ли текущий образ от другого образа (образов)? Да: удовлетворены все зависимости? Да: переход к следующему образу. Нет: удаление образа из RAM в случае стратегии ram-load, но не удалять его из flash. Попытаться загрузить образ из другого слота. Перезапустить проверку зависимости, начиная с первого образа. Нет: перейти к следущему образу.
• Цикл 2. Итерация по всем образам.
Увеличить счетчик безопасности, если это необходимо. Выполнить measured boot и data sharing, если это необходимо.
• Запустить загруженный слот образа 0.
[Swap образа]
Загрузчик делает swap содержимого двух слотов образа по двум причинам:
• Пользователь выдал операцию "set pending" (установить ожидание); образ в secondary должен быть запущен однократно (state I) или с повторениями (state II), в зависимости от того, был ли указан permanent swap. • Тестовый образ перезагружен без подтверждения; загрузчик должен вернуться к исходному образу, который сейчас находится в secondary-слоте (state III).
Если трейлеры образа показывают, что должен быть запущен образ в secondary слоте, то загрузчику надо скопировать содержимое secondary слот в primary slot. При этом текущий образ в primary также должен быть сохранен, чтобы его можно было использовать позже. Кроме того, оба образа должны быть восстанавливаемыми, если загрузчик сбрасывается посередине операции swap. Два образа переставляются по следующей процедуре:
1. Определяется, совместимым ли обра слота, чтобы можно было переставить их образы. Для обеспечения совместимости они оба должны иметь только сектора, которые могут вписаться в область scratch, и если у одного из них есть секторы большего размера, чем у другого, то должна быть возможность подогнать некоторое округленное количество секторов из другого слота. На следующих шагах мы будем использовать терминологию "region" для общего количества копируемых/стираемых данных, потому что это может быть любое количество секторов, в зависимости от того, сколько может поместиться в scratch, чтобы соответствовать какой-либо swap-операции.
2. Итерация по списку индексов региона в порядке убывания (т. е. начиная с самого большого индекса); копируются только те регионы, которые предварительно определены как часть образа; текущий элемент = "индекс".
a. Стирание области scratch. b. Копирование secondary_slot[index] в область scratch. • Если это последний регион в слоте, то в области scratch находится временная область состояния, инициализированная для сохранения начального состояния, поскольку будет стерт последний регион primary слота. В этом случае копируются только данные, которые были вычислены по величине копируемого образа. • Иначе если это первый переставленный регион, но не последний регион в слоте, то инициализируется область статуса в primary слоте, и копируется полное содержимое региона. • Иначе копируется все содержимое региона. c. Записывается обновленный swap status (i). d. Стирается secondary_slot[index]. e. Копируется primary_slot[index] в secondary_slot[index], в соответствии с ранее скопированному на шаге b. • Если это не последний регион в слоте, то стирается трейлер в secondary слоте, чтобы всегда использовать его в primary слоте. f. Записывается обновленный updated swap status (ii). g. Стирается primary_slot[index]. h. Копируется область scratch в primary_slot[index], в соответствии с количеством, которое было скопировано ранее на шаге b. • Если это последний регион в слоте, то status читается из scratch (куда он был временно сохранен) и записывается новый в primary-слот. i. Записывается обновленный swap status (iii).
3. Сохраняется завершение процедуры swap в трейлер образа primary-слота.
Необходима осторожность на шаге 2, чтобы пользователем мог быть позже записан трейлер образа secondary-слота. С незаписанным трейлером образа пользователь может проверить образ в secondary слоте (например переход на state I).
Замечания:
1. Если копируемый регион содержит последний сектор, то swap status временно сохраняется в scratch на время выполнения этой операции, иначе всегда испольуется область primary-слота. 2. Загрузчик пытается копировать только используемые сектора (основываясь на самом большом образе, установленном в любом из слотов), минимизируя тем самым количество копируемых секторов и снижая время, требуемое для операции swap.
Особенности шага 3 варьируются в зависимости от: проверяется ли образ (test), постоянно ли используется (permanent), откатывается ли (revert), или произошла ошибка на проверке secondary-слота, когда был запрошен swap (failure):
* test:
- Запись primary_slot.copy_done = 1
(swap произошел из-за записи следующих значений:
primary_slot.magic = BOOT_MAGIC
secondary_slot.magic = UNSET
primary_slot.image_ok = Unset)
* permanent:
- Запись primary_slot.copy_done = 1
(swap произошел из-за записи следующих значений:
primary_slot.magic = BOOT_MAGIC
secondary_slot.magic = UNSET
primary_slot.image_ok = 0x01)
* revert:
- Запись primary_slot.copy_done = 1
- Запись primary_slot.image_ok = 1
(swap произошел из-за записи следующих значений:
primary_slot.magic = BOOT_MAGIC)
* failure (ошибка проверки secondary-слота):
- Запись primary_slot.image_ok = 1
После завершения операций, как было описано выше, должен быть загружен образ в primary-слоте.
[Swap status]
Регион swap status позволяет загрузчику сделать восстановление в случае, когда он был перезапущен посередине swap-операции образа. Регион swap status состоит из серии однобайтных записей. Эти записи вписываются независимо, и поэтому доложны быть дополнены до минимально допустимого размера одной записи flash (значение min-write-size, что определяется особенностями аппаратуры flash). На рисунке ниже, где показана структура региона swap status, для упрощения сделано предположение, что min-write-size = 1.
Объяснение этой картинки: каждый слот образа разбит на последовательность секторов flash. Если бы мы перечислили сектора в одном слоте, начиная с 0, то у нас был бы список индексов секторов. Поскольку имеется 2 слота образа, каждый индекс сектора будет соответствовать паре секторов. Например, сектор с индексом 0 соответствует первому сектору в primary слоте и первому сектору в secondary слоте. В завершение сделайте обратный список индексов таким образом, чтобы список начинался с индекса BOOT_MAX_IMG_SECTORS-1, и заканчивался на 0. Регион swap status представлен этим обратным списком.
Во время swap-операции каждой индекс сектора переходит через 4 отдельных состояния:
Каждый раз, когда индекс сектора переходит в новое состояние, загрузчик вписывает соответствующую запись в регион swap status. Логически загрузчику нужно делать только одну запись на сектор, чтобы отслеживать текущее состояние swap. Однако из-за того, аппаратурой flash накладываются ограничения, запись не может быть вписана, когда меняется состояние индекса. Чтобы решить эту проблему, загрузчик использует 3 записи на один индекс сектора вместо одной.
Каждая пара сектор-состояние представлена как набор трех записей. Значения записей сопоставляются с вышеупомянутыми четырьмя состояниями следующим образом:
| rec0 | rec1 | rec2
--------+------+------+------
state 0 | 0xff | 0xff | 0xff
state 1 | 0x01 | 0xff | 0xff
state 2 | 0x01 | 0x02 | 0xff
state 3 | 0x01 | 0x02 | 0x03
Регион swap status может уместить BOOT_MAX_IMG_SECTORS индексов сектора. Следовательно размер региона в байтах составит BOOT_MAX_IMG_SECTORS * min-write-size * 3. Единственное требование для подсчета индекса состоит в том, что счетчик должен быть достаточно большим, чтобы учесть самый большой размер образа (т. е. по меньшей мере таким же большим, как общее количество секторов в слоте образа). Если слоты образа сконфигурированы со значением BOOT_MAX_IMG_SECTORS 128, и используют меньше 128 секторов, то первая вписанная запись будет где-то посередине региона. Например, если слот использует 64 сектора, то индекс первого сектора, который переставляется, будет 63, что точно соответствует позиции половины региона.
Замечание: поскольку в области scratch требуется только запись перестановки последнего сектора, она использует максимум min-write-size * 3 байт для своей собственной области статуса.
[Восстановление из сброса]
Если загрузчик был сброшен посередине операции swap, то во flash окажутся 2 разобщенных образа. Bootutil восстановит этот случай с помощью трейлеров образа, чтобы определить, какие части образа распространились по flash.
Первый шаг состоит в том, чтобы определить, где находится соответствующий регион swap status. Из-за того, что этот регион встроен в слоты образа, его место расположения меняется в процессе выполнения операции swap. Приведенный ниже набор таблиц сопоставляет содержимое трейлеров образа с положением swap status. В этих таблицах поле "source" показывает. где находится регион swap status. В случае multi image boot область primary образа и одна область scratch всегда проверяются парами. Если swap status найден в область scratch, то возможно этот статус не принадлежит текущему образу. Поле swap_info из swap status хранит номер соответствующего образа. Если он не совпадает, то будет возвращено "source: none".
| primary slot | scratch |
----------+--------------+--------------|
magic | Any | Good |
copy-done | Any | N/A |
----------+--------------+--------------'
source: scratch |
----------------------------------------'
| primary slot | scratch |
----------+--------------+--------------|
magic | Unset | Any |
copy-done | 0xff | N/A |
----------+--------------+--------------|
source: primary slot |
----------------------------------------+------------------------------+
Это представляет один из двух случаев: |
- Нет никакого swap (нет статуса для чтения, поэтому проверка |
не повредит). |
- Mid-revert; статус находится в primary слоте. |
По этой причине мы подразумеваем primary слот как источник (source), |
чтобы запустить проверку области статуса и определить, был ли |
ли процесc swap-а. |
-----------------------------------------------------------------------'
Если регион swap status покажет, что образы не являются непрерывными, то MCUboot определеит тип операции swap, которая была прервана сбросом, путем чтения поля swap info в трейлере активного образа и извлечением swap type из бит 0-3, после чего возобновит операцию. Другими словами, он применит процедуру, определенную в предыдущей секции, перемещая image 1 в primary слот и image 0 в secondary слот. Если boot status показывает, что часть образа присутствует в области scratch, то эта часть копируется в правильное место, продолжая процесс с шага e или шага step h в swap-процедуре области, в зависимости от того, принадлежит ли часть к image 0 или к image 1.
После того, как операция swap была завершена, заггрузчик заработает так, как если бы он был только что запущен.
[Проверка целостности]
Образ проверяется на целостность (integrity check) перед тем, как он будет скопирован в primary слот. Если загрузчик не выполняет swap образа, то он опционально может выполнить проверку целостности образа в primary слоте, если установлена опция MCUBOOT_VALIDATE_PRIMARY_SLOT, иначе эта проверка не выполняется.
Во время проверки целостности загрузчик проверяет следующие аспекты образа:
• 32-битное число magic должно быть корректным (IMAGE_MAGIC). • Образ должен содержать структуру image_tlv_info, идентифицирующую его magic (IMAGE_TLV_PROT_INFO_MAGIC или IMAGE_TLV_INFO_MAGIC), сразу следующую за firmware (hdr_size + img_size). Если IMAGE_TLV_PROT_INFO_MAGIC найдена, то после ih_protect_tlv_size байт должна присутствовать другая image_tlv_info, эквивалентная IMAGE_TLV_INFO_MAGIC. • Образ должен содержать SHA256 TLV. • Вычисленная SHA256 должна совпадать с содержимым SHA256 TLV. • Образ может содержать сигнатуру TLV. Если это имеет место, то он также должен иметь KEYHASH TLV с хэшем ключа, который использовался для подписи. После этого будет просмотрен список ключей в поиске соответствующего ключа, который затем будет использоваться для проверки содержимого образа.
Для низкопроизводительных MCU, с которыми процесс проверки при загрузке может быть долгим (около 1 .. 2 секунд для arm-cortex-M0), должна использоваться опция MCUBOOT_VALIDATE_PRIMARY_SLOT_ONCE. Она будет кешировать результат проверки, описанный выше, в область magic слота primary. На следующей загрузке проверка будет пропущена, если предыдущая проверка была успешной. Эта опция снижает уровень безопасности, поскольку атакующий может изменить содержимое flash после того, как хороший образ был проверен, и запустить свой собственный образ без его повторной проверки. Так что разрешать эту опцию следует с осторожностью.
[Безопасность]
Как было показано выше, финальный шаг проверки целостности это проверка сигнатуры. У загрузчика может быть один публичный ключ, или их может быть несколько, встроенных во время сборки. При проверке сигнатуры загрузчик проверяет, был ли образ подписан приватным ключом, который соответствует встроенному KEYHASH TLV.
Для информации по встроенным в загрузчик публичным ключам, а также по генерации подписанных образов см. [5]. Если вы хотите разрешить использовать зашифрованные образы, см. [4].
Замечание: шифрование образа не поддерживается, если выбрана стратегия апгрейда direct-xip.
Использование для верификации аппаратных ключей. По умолчанию в код загрузчика публичный ключ встраивается целиком, и его хэш добавляется к манифесту образа как запись KEYHASH TLV. В качестве альтернативы загрузчик может быть сделан независимым от ключей путем установки опции MCUBOOT_HW_KEY. В этом случае для целевого устройства должен быть сконфигурирован хэш публичного ключа, и MCUboot должен иметь возможность извлекать пару key-hash. По этой причине целевая система должна представить функцию boot_retrieve_public_key_hash(), которая декларируется в boot/bootutil/include/bootutil/sign_key.h. Это также требуется для использования полной опции аргумента --public-key-format утилиты imgtool, чтобы добавить весь публичный ключ (PUBKEY TLV) в манифест образа вместо его хэша (KEYHASH TLV). Во время загрузки публичный ключ проверяется перед его использованием для верификации сигнатуры, MCUboot вычисляет хэш публичного ключа из области TLV, и сравнивает его с key-hash, извлеченным из устройства. Этот способ делает MCUboot независимым от публичного ключа (ключей). Ключ (ключи) может быть сконфигурирован в любое время различными сторонами.
[Защищенные TLV]
Если область TLV содержит защищенные записи TLV, то начиная со структуры image_tlv_info с числом magic IMAGE_TLV_PROT_INFO_MAGIC данные этих TLV должны быть также защищены проверкой целостности и подлинности. Помимо полного размера защищенных TLV, сохраненных в image_tlv_info, размер защищенны TLV вместе с размером самой структуры image_tlv_info также сохраняется в поле ih_protected_size внутри заголовка.
Всякий раз, когда образ имеет защищенные TLV, вычисление SHA256 должно происходить не только по заголовку образа, но также и по заголовку информации TLV и защищенным TLV.
A +---------------------+
| Header | < - struct image_header
+---------------------+
| Payload |
+---------------------+
| TLV area |
| +-----------------+ | struct image_tlv_info with
| | TLV area header | | < - IMAGE_TLV_PROT_INFO_MAGIC (опционально)
| +-----------------+ |
| | Protected TLVs | | < - защищенные TLV (структура image_tlv)
B | +-----------------+ |
| | TLV area header | | < - структура image_tlv_info с IMAGE_TLV_INFO_MAGIC
C | +-----------------+ |
| | SHA256 hash | | < - хэш от A - B (структура image_tlv)
D | +-----------------+ |
| | Keyhash | | < - показывает публичный ключ для подписи (структура image_tlv)
| +-----------------+ |
| | Signature | | < - подпись из C - D (структура image_tlv), только хэш
| +-----------------+ |
+---------------------+
[Проверка зависимостей]
MCUboot может поддерживать несколько образов firmware. Он может обновлять их независимо друг от друга, однако во многих случаях может потребоваться описать зависимости между образами (например, чтобы обеспечить совместимость API образов и избежать проблем в их совместной работе).
Зависимости между образами могут быть описаны в дополнительных записях TLV, а защищенной области TLV, в конце образа. Может быть больше одной записи о зависимости, однако на практике если платформа поддерживает только 2 отдельных образа, то здесь может быть только одна запись, которая отражает зависимость для другого образа.
На фазе проверки зависимости (dependency check) все прерванные swap-ы завершены, если таковые имели место. Во время проверки зависимости загрузчик проверяет, удовлетворены ли все зависимости. Если как минимум одна из зависимостей не удовлетворена, то тип swap этого образа должен быть соответственно изменен, и проверка зависимости должна быть перезапущена. Таким образом, количество неудовлетворенных зависимостей уменьшится, или останется прежним. Всегда существует как минимум одна допустимая конфигурация. В самом худшем случае после проверки зависимостей система возвращается в исходное состояние.
Для дополнительной информации по добавлению записей описания зависимости в образ см. документацию imgtool [2].
[Защита от даунгрейда]
Защита от обновления "вниз" (downgrade) это функция, которая требует, чтобы нвый образ должен иметь большее значение счетчика version/security чем заменяемый образ, предотвращая тем самым злонамеренное понижения версии ПО устройства до более старой, уязвимой версии firmware.
Программная защита от даунгрейда. Программная защита от даунгрейда основывается на сравнении номеров версий образа. Эта функция разрешается опцией MCUBOOT_DOWNGRADE_PREVENTION. В этом случае защита от даунгрейда доступна только когда используется стратегия обновления, основанная на перезаписи, overwrite-based (т. е. установлена опция MCUBOOT_OVERWRITE_ONLY).
Аппаратная защита от даунгрейда. Каждый подписанный образ может содержать счетчик безопасности в своей защищенной области TLV, который может быть добавлен к образу опцией -s скрипта imgtool. Во время аппаратной защиты от даунгрейда (что также называют защитой от отката, rollback protection) счетчик безопасности нового образа сравнивается со счетчиком активного образа, который должен быть сохранет в энергонезависимом и доверенном компоненте устройства. Целесообразно обрабатывать этот счетчик отдельно от номера версии образа:
• Его необязательно нужно увеличивать с каждым новым релизом ПО. • Он делает возможным делать в некой степени и даунгрейд: если счетчик безопасности имеет такое же значение, то образ принимается.
Это опциональный шаг процесса проверки образа, и он может быть разрешен опцией конфигурации MCUBOOT_HW_ROLLBACK_PROT. Когда эта опция разрешена, целевая система должна предоставить реализацию интерфейса счетчика безопасности, определенного в boot/bootutil/include/security_cnt.h.
[Measured boot и data sharing]
MCUboot предоставляет механизм предоставления в общий доступ информации статуса загрузки (sharing boot status), что также известно ака "measured boot", и интерфейс для предоставления в общий доступ специфичной для приложения информации, к которой может обратиться ПО во время выполнения (runtime). Если любая из следующих опций разрешена, то целевая система должна предоставить область общедоступных данных (shared data) между загрузчиком и runtime firmware, и определить следующие парамерты:
В области общедоступной памяти (shared memory) все элементы данных сохраняются в формате тип-длина-значение (type-length-value, TLV). Перед добавлением первого элемента данных вся область перезаписывается нулями, и добаляется заголовок TLV в начало этой области во время фазы инициализации. Этот заголовок TLV содержит поле tlv_magic со значением SHARED_DATA_TLV_INFO_MAGIC, и поле tlv_tot_len, которое показывает общую длину области shared TLV, включая этот заголовок. За заголовком идут записи данных TLV, которые состоят из заголовка shared_data_tlv_entry и самих данных. В заголовке данных имеется поле tlv_type, которое идентифицирует потребителя записи (в ПО runtime), и определяющее subtype этого элемента данных. Дополнительную информацию про поле tlv_type и типы данных можно найти в boot/bootutil/include/bootutil/boot_status.h. За типом идет поле tlv_len, которое показывает размер элемента данных в байтах, не включая заголовок элемента. После этой структуры заголовка идут реальные данные.
/** Заголовок shared-данных TLV. Все поля в формате little endian. */
struct shared_data_tlv_header {
uint16_t tlv_magic;
uint16_t tlv_tot_len; /* размер области TLV (включая этот заголовок) */
};
/** Формат заголовка элемента shared-данных TLV. Все поля в формате little endian. */
struct shared_data_tlv_entry {
uint16_t tlv_type;
uint16_t tlv_len; /* длина TLV-данных (не включая этот заголовок). */
};
Функционал measured boot может быть разрешен опцией конфигурации MCUBOOT_MEASURED_BOOT. Когда она разрешена, также должен использоваться аргумент --boot_record скрипта imgtool во время процесса подписи образа, чтобы добавить BOOT_RECORD TLV к манифесту образа. Этот TLV содежит следущие атрибуты/измерения (attributes/measurements) образа в формате кодирования CBOR:
• Software type (тип ПО, роль программного компонента). • Версия ПО. • Signer ID (идентифицирут центр подписи). • Measurement value (хэш образа) • Measurement type (алгоритм, используемый для вычисления measurement value).
Строка sw_type, переданная в параметре опции --boot_record, будет значением атрибута “Software type” в генерируемом BOOT_RECORD TLV. Целевая система также должна определить макрос MAX_BOOT_RECORD_SZ, который показывает максимальный размер CBOR encoded boot record в байтах. Во время процесса загрузки MCUboot будет искать в манифесте активных образов (в случае нескольких образов) эти элементы TLV (выбирая последний и проверенный образ), и копировать CBOR encoded binary data в область shared data. Сохранение всех этих атрибутов образа с момента стадии загрузки до последующего использования runtime-сервисами (такими как attestation service) известно как measured boot.
Установка опции MCUBOOT_DATA_SHARING разрешает предоставлять в общий доступ данные, специфичные для приложения, используя ту же самую область shared data, что и для measured boot. Для этого целевая система должна предоставить определение для функции boot_save_shared_data(), которая декларируется в boot/bootutil/include/bootutil/boot_record.h. Функция boot_add_data_to_shared_area() может использоваться для добавления новых элементов TLV в область shared data.
[Тестирование в CI]
Testing Fault Injection Hardening (FIH). CI в настоящее время тестирует функцию Fault Injection Hardening кода MCUboot путем выполнения инструкции пропуска, и последующего анализа, был ли загружен поврежденный образ, или нет.
Основная идея состоит в том, что пропуск инструкции можно автоматизировать, создав сценарий отладчика, выполняющего следующие шаги:
• Установить breakpoint по указанному адресу. • Продолжить выполнение. • При срабатывании breakpoint увеличить Program Counter. • Продолжить выполнение. • Отключиться от target после достижения тайматута.
Вопрос состоит в том, был ли загружен поврежденный образ, или нет, можно решить, выполнив поиск определенных записей в логе скрипта.
Поскольку MCUboot встраивается в микроконтроллер тестирование FI не будет иметь большого смысла в среде симулятора, работающего на машине хоста с другой архитектурой, отличающейся от целевого MCU, так как степень защиты (и её проверки) зависит от поведения компилятора. Например (несколько не интуитивно) код, сгенерированный gcc с опцией оптимизации -O0, более устойчив к атакам FI по сравнению с кодом, сгенерированным с оптимизациями -O3 или -Os.
Для запуска желаемой архитектуры в CI тесты должны быть выполнены на эмуляторе (поскольку реальные устройства недоступны в окружении CI). Для этой реализации был выбран QEMU.
Для тестирования MCUboot нужен набор драйверов и реализация функции main. Для этой цели был выбран тест Trusted-Firmware-M, как поддерживающий платформы Armv8-M, которые также эмулируются в QEMU.
Тесты запускаются в контейнере docker внутри виртуальных машин (CI VM), чтобы упростить установку окружения сборки и тестирования (QEMU, компиляторы, интерпретаторы). Виртуальные машины CI VM, похоже используют довольно старую версию Ubuntu (16.04).
Последовательность тестирования можно проиллюстрировать следующим (псевдо-кодом):
fn main()
# Реализовано в скрипте ci/fih-tests_install.sh
generate_docker_image(Dockerfile)
# Подробности см. ниже. Реализовано в ci/fih-tests_run.sh.# Вызов функции с разными параметрами выполняется на основе Travis CI,# значения предоставляются в .travis.yaml.
start_docker_image(skip_sizes, build_type, damage_type, fih_level)
fn start_docker_image(skip_sizes, build_type, damage_type, fih_level)
# Реализовано в ci/fih_test_docker/execute_test.sh
compile_mcuboot(build_type)
# Реализовано в ci/fih_test_docker/damage_image.py
damage_image(damage_type)
# Реализовано в ci/fih_test_docker/run_fi_test.sh
ranges = generate_address_ranges()
for s in skip_sizes
for r in ranges
do_skip_in_qemu(s, r) # См. подробности ниже
evaluate_logs()
fn do_skip_in_qemu(size, range)
for a in r
run_qemu(a, size) # См. подробности ниже
# Эта часть реализована в ci/fih_test_docker/fi_tester_gdb.sh
fn run_qemu(a, size)
script = create_debugger_script(a, size)
start_qemu_in_bacground() # logs serial out to a file
gdb_attach_to_qemu(script)
kill_qemu()
# Это проверяет логи отладчика и QUEMU, и принимает решение, успешно# или нет прошел тест, и когда образ был загружен, а когда нет. Затем# в стандартный вывод выпускается фрагмент yaml, чтобы он был обработан# вызывающим скриптом.
evaluate_run(qemu_log_file)
Дополнительные замечания:
• Образ повреждается изменением его сигнатуры. • MCUBOOT_FIH_PROFILE_MAX не тестировалось, поскольку требует TRNG, и платформа AN521 не имеет поддержки для этого. Однако этот профиль добавляет случайную задержку выполнения к коду, что не должно слишком сильно повлиять на результаты пропуска инструкции, поскольку точка останова помещается по точному адресу. Однако на практике бывает трудно выдерживать тайминги атак. • Тестовые случаи, упределеные в .travis.yml всегда возвратят успех (passed), если они были успешно выполнены. Создается yaml-файл во время выполнения теста, который содержит результаты тестирования выполнения. Суммарная сводка собранных результатов печатается в лог по окончании теста.
Достоинство тестов в образе докера в том, что можно запустить тесты на локальной машине, где есть git и docker, без необходимости устанавливать дополнительный софт.
Запуск теста на хосте выглядит следующим образом (команды ниже выдаются из директории исходного кода MCUboot):
Переменные окружения travis CI в последней команде установлены на основе конфигураций, предоставленных в .travis.yaml.
Эти команды запустят тесты, однако шелл здесь не интерактивный, и нельзя видеть результаты во время работы теста. Чтобы получить интерактивный шелл, где можно проверять результаты, надо сделать следующее:
• Образ docker должен быть собран с ci/fih-tests_install.sh, как описано выше. • Запустить образ docker следующей командой: docker run -i -t mcuboot/fih-test. • Выполнить тест командой, подобной следующей: /root/execute_test.sh 8 RELEASE SIGNATURE MEDIUM. После завершения теста произойдет возврат в shell, и можно будет исследовать результаты. Также можно остановить тест нажатием Ctrl+c. Параметры для execute_test.sh: SKIP_SIZE, BUILD_TYPE, DAMAGE_TYPE, FIH_LEVEL в таком порядке.