[i] Работа с криптопроцессором. Программирование OTP
ВВЕДЕНИЕ
В микроконтроллере К1986ВК01GI реализованы три ядра: два Cortex-M4, и одно специальное - криптографический модуль (сопроцессор), построенный на базе Cortex-M0.
В материале Начало работы с криптоядром микроконтроллера «К1986ВК01GI» подробно рассказано о том, как начать работать с криптоядром и загрузить в него программу для запуска из внутренней памяти, через отладочный интерфейс SWD.
В данной статье рассмотрим, как загружать в криптоядро программу для исполнения с помощью основного ядра M4, то есть именно так, как в «Release» версии программы для микроконтроллера.
СТРУКТУРА ВЗАИМОДЕЙСТВИЯ. ШЛЮЗ
Рисунок 1 – Структурная схема микроконтроллера К1986ВК01GI
Стоит отметить, что на данной схеме не отражены отладочные интерфейсы. Как отмечено было ранее, в защищенном криптомодуле реализован отладочный интерфейс SWD. Он доступен на следующих портах ввода – вывода:
SWDIO – PC30;
SWCLK – PC31;
Но в «Release» варианте программы пользоваться отладчиком возможности нет, поэтому в первую очередь рассмотрим взаимодействие через шлюз. Шлюз представляет собой:
-
16 входных/выходных регистров (8 входных и 8 выходных. Следовательно, входной с одной стороны является выходным с другой),
-
2 FIFO данных: от открытого ядра к защищённому, и от защищённого ядра к открытому, шириной данных 32 бита и объёмом 16 слов.
Реализовано по 3 служебных регистра: для FIFO и для входных/выходных регистров. Они необходимы для контроля и настройки взаимодействия обмена данными по шлюзу. Подробная карта регистров блока шлюза представлена в таблице 1.
Таблица 1 – Карта регистров блока Шлюза
Открытая сторона M4 | Название регистра | Закрытая сторона M0 | ||
Доступ | Описание регистра | Описание регистра | Доступ | |
w/o |
Регистр записи во входное FIFO к защищённой стороне. |
OP_FIFO_OP_TO_SF | Регистр чтения из входного FIFO от открытой стороны. | r/o |
r/o |
Регистр чтения из выходного FIFO от защищённой стороны. |
OP_FIFO_SF_TO_OP | Регистр записи в выходное FIFO к открытой стороне. | w/o |
r/w | Регистр задания контрольных уровней заполненности FIFO. | OP_FIFO_LEVELS | Регистр задания контрольных уровней заполненности FIFO. | r/w |
r/w | Регистр задания маски разрешения источников запроса прерывания FIFO. | OP_FIFO_INT_MASK | Регистр задания маски разрешения источников запроса прерывания. | r/w |
r/w | Регистр индикации и очистки источников запроса прерывания FIFO. | OP_FIFO_INT_SOURCE | Регистр индикации и очистки источников запроса прерывания. | r/w |
r/w | Входной регистр. | OP_REG_0 | Выходной регистр. | r/o |
r/w | Входной регистр. | OP_REG_1 | Выходной регистр. | r/o |
r/w | Входной регистр. | OP_REG_2 | Выходной регистр. | r/o |
r/w | Входной регистр. | OP_REG_3 | Выходной регистр. | r/o |
r/w | Входной регистр. | OP_REG_4 | Выходной регистр. | r/o |
r/w | Входной регистр. | OP_REG_5 | Выходной регистр. | r/o |
r/w | Входной регистр. | OP_REG_6 | Выходной регистр. | r/o |
r/w | Входной регистр. | OP_REG_7 | Выходной регистр. | r/o |
r/o | Выходной регистр. | OP_REG_8 | Входной регистр. | r/w |
r/o | Выходной регистр. | OP_REG_9 | Входной регистр. | r/w |
r/o | Выходной регистр. | OP_REG_10 | Входной регистр. | r/w |
r/o | Выходной регистр. | OP_REG_11 | Входной регистр. | r/w |
r/o | Выходной регистр. | OP_REG_12 | Входной регистр. | r/w |
r/o | Выходной регистр. | OP_REG_13 | Входной регистр. | r/w |
r/o | Выходной регистр. | OP_REG_14 | Входной регистр. | r/w |
r/o | Выходной регистр. | OP_REG_15 | Входной регистр. | r/w |
r/o | Регистр статуса занятости входных регистров. | OP_REGS_BUSY | Регистр статуса занятости входных регистров. | r/o |
r/w | Регистр задания маски разрешения источников запроса прерывания REG_x. | OP_REGS_INT_MASK | Регистр задания маски разрешения источников запроса прерывания REG_x. | r/w |
r/w | Регистр индикации и очистки источников запроса прерывания REG_x. | OP_REGS_INT_SOURCE | Регистр индикации и очистки источников запроса прерывания REG_x. | r/w |
Общий алгоритм добавления исполняемой программы в память криптографического сопроцессора представлен на рисунке 2.
Рисунок 2 – Общий алгоритм загрузки программы для криптоядра с помощью шлюза
Алгоритм
-
Подготовить программу для работы сопроцессора M0.
-
Конвертировать программу в массив данных.
-
К этому массиву необходимо добавить Hash и наложить гамму.
После чего новый массив данных уже можно использовать в программе для ядра M4.
-
В основной программе для открытого ядра необходимо разрешить тактирование M0, после чего ядро M4 может приступить к передаче зашифрованных данных через шлюз в ядро M0.
-
Предварительно систему криптомодуля необходимо проинициализировать, а именно, записать серийный номер, записать гамму для расшифровки получаемой программы. Этот процесс осуществляется с помощью UART – загрузчика, выведенного на выводы:
A1 / UARTTX (CRPT_UARTTX);
B7 / UARTRX (CRPT_UARTRX).
СТАРТ И ИНИЦИАЛИЗАЦИЯ КРИТПОЯДРА
После разрешения тактирования со стороны M4, ядро M0 запускает внутреннюю бутовую программу. Как уже было отмечено, для взаимодействия с микроконтроллером в загрузочной программе ядра M0 используется модуль UART. Поэтому для анализа данных, получаемых от микроконтроллера, можно воспользоваться любой терминальной программой.
Необходимо запустить программу в M4, чтобы она разрешила тактирование криптоядра. При этом в качестве источника тактирования лучше использовать генератор HSE для обеспечения стабильной работы UART, поскольку подстройки скорости во внутреннем boot не реализовано, а генератор HSI неточный (f_HSI=6-10МГц) - его частота может варьироваться от образца к образцу.
После старта загрузочная программа ядра Cortex-M0 начинает выполнять чтение статуса из OTP памяти по определенному адресу (0x1010C000) и анализирует его:
- если статус = 0x0, загрузчик приступает к процедуре преинициализации OTP;
-
если статус = 0x0000A55A, загрузчик приступает к процедуре инициализации OTP;
-
если статус = 0x75A9A55A, загрузчик понимает, что OTP инициализирована и приступает к процедуре обработки данных, полученных через шлюз от ядра M4.
Таким образом, есть 3 фазы работы встроенного загрузчика:
1. Фаза преинициализации - запись в одноразово-записываемую память (OTP) уникального серийного номера. Серийный номер имеет длину 8 байт. Запись серийного номера строго однократна. После завершения записи фаза жизненного цикла изменяется и этот функционал более никогда не будет доступен. Записанный серийный номер защищается контрольной суммой CRC32, ошибка контрольной суммы серийного номера при дальнейшей работе трактуется как угроза безопасности и приводит к блокированию дальнейшей работы защищённого ядра.
2. Фаза инициализации - загрузка и запись случайной последовательности (гаммы) в однократно программируемую память. После завершения записи фаза жизненного цикла изменяется и этот функционал более никогда не будет доступен.
3. Фаза работы - загрузчика защищённого ядра обеспечивает загрузку прошивки ядра из памяти незащищённой части через шлюз.
Выполняется проверка целостности серийного номера и его отправка в незащищённое ядро.
Выполняется проверка целостности серийного номера и гаммы, после чего серийный номер отправляется через шлюз в незащищённое ядро.
По готовности в регистрах шлюза выставляется флаг готовности. Прочитав этот флаг программное обеспечение незащищённого ядра должно загрузить в шлюз зашифрованную прошивку.
Загрузчик защищённого ядра проверяет целостность зашифрованной прошивки, сравнивая её CRC32. Если проверка выполнена успешна, то происходит расшифровывание прошивки в оперативной памяти путём её сложения с гаммой по модулю 2.
Целостность расшифрованной прошивки определяется путём расчёта и сравнения с расшифрованным значения хэш-функции RIPEMD-160.
Если целостность прошивки подтверждена, выставляется флаг готовности и управление передаётся расшифрованной прошивке в оперативной памятиРассмотрим каждую процедуру по отдельности.
Преинициализация
По сути, данная процедура запускается всегда, когда OTP-память пустая. Первым этапом происходит настройка UART:
-
UART настраивается на скорость 9600 бод при условии, что частота тактирования микроконтроллера 8 МГц.
- Затем по UART загрузчик отправляет статус о том, что ядро готово к преинициализации и выдает статус CM0_PREINIT_READY = 0x3C5A5C0F.
Рисунок 3 – Окно терминала, получение статуса готовности преинициализации
Ниже приведены все расшифровки статусов загрузчика:
// Base CM0 UART/GATE statuses
#define CM0_OK ((CM0_STATUS)0x3C5A0000)
#define CM0_FAIL ((CM0_STATUS)0xC3A50000)
// preinit-init-work statuses
#define CM0_PREINIT_READY ((CM0_STATUS)(0x5C0F | CM0_OK))
#define CM0_PREINIT_DONE ((CM0_STATUS)(0xC535 | CM0_OK))
#define CM0_PREINIT_FAIL ((CM0_STATUS)(0x533A | CM0_FAIL))
#define CM0_INIT_READY ((CM0_STATUS)(0xF05F | CM0_OK))
#define CM0_INIT_DONE ((CM0_STATUS)(0xC335 | CM0_OK))
#define CM0_INIT_FAIL ((CM0_STATUS)(0xFC3A | CM0_FAIL))
#define CM0_GAMMA_READY ((CM0_STATUS)(0x0A59 | CM0_OK))
#define CM0_WORK_READY ((CM0_STATUS)(0xF535 | CM0_OK))
#define CM0_WORK_DONE ((CM0_STATUS)(0xA50A | CM0_OK))
Затем ядро M0 ожидает приём по UART 8 байт серийного номера и 4 байта контрольной суммы CRC данного номера. После получения этих данных МК самостоятельно рассчитывает CRC от полученного серийного номера и сравнивает со значением, полученными по UART. В случае если значения не сошлись, то загрузчик возвращает статус CM0_PREINIT_FAIL. На рисунке 4 показан пример ввода серийного номера SN и некорректно рассчитанной к нему контрольной суммы CRC, в результате чего получен ожидаемый статус о неудачной процедуре преинициализации.
Рисунок 4 – Загрузчик вернул статус неудачной преинициализации
После возвращения статуса PREINIT_FAIL микроконтроллер зависает в бесконечном цикле, повторно дублируя данный статус в шлюз к ядру M4 через 15 регистр. Требуется выполнить сброс, чтобы повторно провести процедуру преинициализации с корректно рассчитанной контрольной суммой. При отправке корректной контрольной суммы микроконтроллер выдаст статус PREINIT_DONE – рисунок 5.
Рисунок 5 – Получение статуса об успешной преинициализации
После выдачи статуса об успешной процедуре преинициализации микроконтроллер записывает во внутреннюю OTP-память статус 0x0000A55A, и CRC, рассчитанное от статуса и серийного номера, после чего вновь зависает. Важно отметить, что в результате успешной или неуспешной процедуры преинициализации микроконтроллер безальтернативно зависает. Разница состоит в том, что, когда процедура выполняется успешно, происходит запись статуса в OTP-память, который анализируется при старте загрузчика. То есть наличие данных 0x0000A55A в статусе позволяет перейти к процедуре инициализации.
Инициализация
После сброса процессор, прочитав измененный статус во внутренней OTP-памяти, приступил к процедуре инициализации, сообщив об этом по UART, выдавая статус INIT_READY – рисунок 6. Дополнительно в 8 и 9 регистры шлюза загрузчик отправил серийный номер, записанный в его OTP-памяти во время преинициализации.
Рисунок 6 – Готовность процедуры инициализации
Затем необходимо передать загрузчику информацию о том, что гамма сформирована, то есть отправить статус GAMMA_READY. В ответ ожидается серийный номер, записанный в OTP-память на предыдущем этапе – рисунок 7. В случае же некорректной передачи статуса, в ответ поступит сообщение INIT_FAIL – рисунок 8.
Рисунок 7 - Выдача серийного номера, в случае получения статуса GAMMA_READY
Рисунок 8 - Выдача статуса INIT_FAIL
После успешной выдачи серийного номера микроконтроллер ожидает принять гамму размером 16364 байта. Это обязательное условие загрузчика. В рассматриваемом случае, к примеру, в качестве гаммы было выбрано число 42.
Рисунок 9 - Отправка гаммы
После принятия гаммы загрузчик микроконтроллера ожидает получить контрольную сумму CRC для переданной гаммы, после чего её проверяет. Если контрольная сумма не сошлась, в ответ загрузчик вернет статус INIT_FAIL – рисунок 10.
Рисунок 10 - Некорректный ввод CRC для гаммы
В случае передачи корректной CRC микроконтроллер приступает к прошивке гаммы во внутреннюю OTP-память, затем изменяет и переписывает в памяти OTP основной статус, который опрашивается при старте процессора на OTP_INIT 0x75A9A55A. После чего рассчитывает новое значение CRC от статуса, серийного номера и гамма, и выдает в UART статус INIT_DONE, что свидетельствует об окончании процесса инициализации – рисунок 11. И, как было отмечено ранее, микроконтроллер опять зависнет даже в случае успешной инициализации и вновь потребует сброса.
Рисунок 11 - Завершение процесса инициализации
После окончания процесса инициализации при последующем сбросе, загрузчик прочитает статус из своей памяти OTP_INIT 0x75A9A55A, что говорит о том, что он готов принимать зашифрованную по прописанной гамме программу, для последующей расшифровки и записи.
Если подключиться с помощью отладчика к ядру M0, то можно посмотреть, как выглядят записанные данные в OTP-памяти – рисунок 12.
Рисунок 12 - Отображение OTP-памяти ядра M0
ЗАПУСК ПРОГРАММЫ НА КРИПТОЯДРЕ
Закончив этап подготовки, взаимодействуя с защищенной системой с помощью uart – загрузчика, была прошита OTP-память. Криптоядро подготовлено к работе. Теперь каждый раз при старте, как уже отмечалось чуть выше, ядро M0 будет анализировать записанные в его память данные, после чего в регистр 15 шлюза выставит статус WORK_READY. Затем ядро может начать загружать прошивку через шлюз, куда её передает ведущее ядро M4.
Подготовка
Алгоритм взаимодействия между ядрами с помощью шлюза приведен выше на рисунке 2. В рамках демонстрации рассмотрим его на конкретном примере: будем накладывать гамму и HASH на прошивку для ядра M0 внутри программы для M4. Это отображено на измененной схеме алгоритма на рисунке 13.
Рисунок 13 - Алгоритм, реализованный на примере в приложенииЗапуск
К данной статье приложено два проекта:
1) Loader – проект с программой для ведущего ядра Cortex-M4, в котором разрешается тактирование криптоядра, а затем шифруется по гамма и передается через шлюз программа для исполнения ядром Cortex-M0;
2) CryptoHello – программа для исполнения ядром Cortex-M0.
Во втором проекте (CryptoHello) конфигурируется модуль UART и отправляется приветствие. Затем включается блок генератора случайных чисел, который генерирует число и отправляет его всё по тому же UART, а также дублирует его в 14 регистр шлюза.
После того как программа для Cortex-M0 была написана, необходимо сгенерировать файл ramCode.h. В приложенном проекте после процесса компиляции выставлена опция запуска файла формата *.bat для генерации файла ramCode.h (рисунок 14).
Рисунок 14 - Выставление настройки для генерации массива после процесса компиляции
То есть программа для исполнения ядром M0 была конвертирована в массив данных. Полученный файл ramCode.h необходимо добавить в проект для ядра Cortex-M4.
Теперь можно перейти к запуску первого проекта (Loader). Стоит вновь обратить внимание на разрешение тактирования криптоядра:
CLK_CNTR->KEY = 0x8555AAA1;
CLK_CNTR->CRPT_CLK |= (1<<28)|(1<<16); // HSE0
Затем в проекте происходит наложение HASH и Gamma на массив чисел из файла ramCode.h, который был добавлен в проект. После чего в основном цикле программы проверяется 15-ый регистр шлюза, куда криптоядро, как отмечено ранее, должно выставить статус WORK_READY.
Рисунок 15 - Выставление статуса WORK_READY (REG15) и ключа (REG08 и REG09) в шлюзТеперь ядро M0 готово принимать защищенную по гамма прошивку. Анализируя готовность, ядром M4 отправляется через шлюз подготовленный заранее код.
С помощью выводов UART криптоядра наблюдаем работу программы – рисунок 16.
Рисунок 16 - Демонстрация работы программы критпоядра
Приложение 1. Алгоритм расчёта CRC
Для расчёта CRC для ключа и гамма использовался алгоритм CRC32. Функция расчёта приведена ниже, а также приложена во вложении к материалу.crc = 0;
crc = crc32(crc, (uint8_t*)sn, 8); //sn - serial nember
uint32_t crc32(uint32_t ctx, const uint8_t* buf, uint32_t sz)
{
ctx = ctx ^ 0xFFFFFFFF;
while (sz--)
{
ctx = crc32_tab[(ctx ^ *(buf++)) & 0xFF] ^ (ctx >> 8);
}
return ctx ^ 0xFFFFFFFF;
}
Приложение 2. Блокировка отладочного интерфейса криптоядра
Важной особенностью защищенной системы является отсутствие возможности проникнуть в неё извне. Одним из первостепенных механизмов защиты является блокировка отладочного интерфейса защищенной подсистемы. Блокировку целесообразно осуществлять после отладки приложения (пользовательской программы). Такая возможность в системе M0 есть. Реализуется она с помощью записи защитного слова в память OTP по адресу 0x10100000.
#define OTP_BASE ((uint32_t)0x10100000U)
#define OTP_MEM_BASE (OTP_BASE)
#define OTP_MEM ((uint32_t *)OTP_MEM_BASE)
OtpData* const gOtpMem = (OtpData*)OTP_MEM;
typedef struct
{
OtpUserData UData; /*< User space */
OtpServiceData SData; /*< Service space */
uint32_t Gamma[SZ_GAMMA]; /*< Gamma arrea */
} OtpData;
typedef struct
{
uint32_t Special; /*< Special word */
uint32_t NotUsed[255]; /*< Testing arrea */
uint32_t User[12032]; /*< User space */
} OtpUserData;
По данному адресу записывается SecurityWord = (0xFFFFFF<<8)|(0xFF<<0). То есть в исполняемой (конечной) программе для ядра M0 можно предусмотреть алгоритм проверки. Сама проверка заключается в чтении защитного слова. В случае если оно не записано, то происходит его запись и, соответственно, блокировка интерфейса SWD.
#include "otpData.h"
int main (void)
{......
otpRead(&readOtpWord,&gOtpMem->UData.Special,1);
if ( readOtpWord == SecurityWord)
uartWrite ("\r\n JTAG is already blocked! \r\n", 30);
else
{
otpWrite(&gOtpMem->UData.Special, &SecurityWord, 1);
uartWrite ("\r\n JTAG blocked! \r\n", 15);
}
......
}
Временная блокировка SWD-интерфейса криптоядраВыводы CRPT_SWD совмещены с выводами PC[31:30], при этом после подачи тактирования на защищённую подсистему в регистре CRPT_CLK_CTRL (асинхронный тип регистра), выводы PC[31:30] полностью переходят под управление интерфейса SWD (Cortex-M0) и не могут управляться контроллером портов ввода-вывода и другими периферийными блоками из открытой подсистемы. На время отладки ПО, помимо блокировки SWD через OTP, есть возможность отключить в защищенной подсистеме интерфейс SWD, чтобы выводы PC[31:30] снова стали доступны для управления из открытой подсистемы. Для этого в ПО для Cortex-M0 необходимо сбросить бит 7 (DEBUG_EN) регистра SYS->CTRL:
SYS->CTRL &= ~(1<<7);
После сброса бита DEBUG_EN выводы PC[31:30] снова перейдут под управление открытой подсистемы (Cortex-M4F). Чтобы снова включить интерфейс SWD ядра Cortex-M0 необходимо выполнить сброс МК, программным способом установить бит DEBUG_EN для включения SWD нельзя.
Приложение 3. Программа для инициализации OTP памяти криптоядра
Проект mldr149secLoader реализует взаимодействие с загрузчиком защищённой части микросхемы CM0 по UART.
Сохранить статью в PDF