[i] Работа с криптопроцессором. Программирование OTP

ID статьи: 48705
Дата последнего изменения: 15.09.2022 13:29:37

ВВЕДЕНИЕ

В микроконтроллере К1986ВК01GI реализованы три ядра: два Cortex-M4, и одно специальное - криптографический модуль (сопроцессор), построенный на базе Cortex-M0.
В материале Начало работы с криптоядром микроконтроллера «К1986ВК01GI» подробно рассказано о том, как начать работать с криптоядром и загрузить в него программу для запуска из внутренней памяти, через отладочный интерфейс SWD.
В данной статье рассмотрим, как загружать в криптоядро программу для исполнения с помощью основного ядра M4, то есть именно так, как в «Release» версии программы для микроконтроллера.

СТРУКТУРА ВЗАИМОДЕЙСТВИЯ. ШЛЮЗ

На рисунке 1 приведена общая схема микроконтроллера, которая показывает, что всё взаимодействие между ядрами M4 и защищенным M0 осуществляется через шлюз обмена данных.


Рисунок 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 – Общий алгоритм загрузки программы для криптоядра с помощью шлюза

Алгоритм

  1. Подготовить программу для работы сопроцессора M0.

  2. Конвертировать программу в массив данных.

  3. К этому массиву необходимо добавить Hash и наложить гамму. 

Рассчитывать хэш-функцию загрузчика пользовательского ПО необходимо по алгоритму RIPEMD-160.

После чего новый массив данных уже можно использовать в программе для ядра M4. 

  1. В основной программе для открытого ядра необходимо разрешить тактирование M0, после чего ядро M4 может приступить к передаче зашифрованных данных через шлюз в ядро M0.

  2. Предварительно систему криптомодуля необходимо проинициализировать, а именно, записать серийный номер, записать гамму для расшифровки получаемой программы. Этот процесс осуществляется с помощью 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) и анализирует его: 

Таким образом, есть 3 фазы работы встроенного загрузчика:

1. Фаза преинициализации - запись в одноразово-записываемую память (OTP) уникального серийного номера. Серийный номер имеет длину 8 байт. Запись серийного номера строго однократна. После завершения записи фаза жизненного цикла изменяется и этот функционал более никогда не будет доступен. Записанный серийный номер защищается контрольной суммой CRC32, ошибка контрольной суммы серийного номера при дальнейшей работе трактуется как угроза безопасности и приводит к блокированию дальнейшей работы защищённого ядра. 

2. Фаза инициализации - загрузка и запись случайной последовательности (гаммы) в однократно программируемую память. После завершения записи фаза жизненного цикла изменяется и этот функционал более никогда не будет доступен.

 3. Фаза работы - загрузчика защищённого ядра обеспечивает загрузку прошивки ядра из памяти незащищённой части через шлюз.

Выполняется проверка целостности серийного номера и его отправка в незащищённое ядро.

Выполняется проверка целостности серийного номера и гаммы, после чего серийный номер отправляется через шлюз в незащищённое ядро.

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

Загрузчик защищённого ядра проверяет целостность зашифрованной прошивки, сравнивая её CRC32. Если проверка выполнена успешна, то происходит расшифровывание прошивки в оперативной памяти путём её сложения с гаммой по модулю 2.

Целостность расшифрованной прошивки определяется путём расчёта и сравнения с расшифрованным значения хэш-функции RIPEMD-160.

Если целостность прошивки подтверждена, выставляется флаг готовности и управление передаётся расшифрованной прошивке в оперативной памяти

Рассмотрим каждую процедуру по отдельности.

Преинициализация

По сути, данная процедура запускается всегда, когда OTP-память пустая. Первым этапом происходит настройка UART: 

  1. UART настраивается на скорость 9600 бод при условии, что частота тактирования микроконтроллера 8 МГц.

  2. Затем по 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 отправляется через шлюз подготовленный заранее код.
Время до готовности M0 запускать программу составляет порядка 3~4 сек на частоте 8 МГц. Данное время тратится на отработку загрузчика.

С помощью выводов 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.
Если защищенное слово записано в OTP-памяти, блокировка отладочного интерфейса SWD происходит сразу при подаче питания, что реализует 100% защиту.
Следовательно, после отработки основного алгоритма исполняемой программы в лабораторных условиях необходимо запустить алгоритм записи SecurityWord, что приведет к блокировке отладочного интерфейса защищенной подсистемы. В следующих циклах запуска работы системы при успешном чтение SecurityWord дополнительное обращение к OTP-памяти происходить не будет. Ниже приведён пример алгоритма блокировки интерфейса, который может быть встроен в программу для ядра M0 (проект CryptoHello). В конечное решение о том, как организовать блокировку отладочного интерфейса защищенной подсистемы, разработчик изделия принимает самостоятельно.

 #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.

Контактная информация

Сайт:https://support.milandr.ru
E-mail:support@milandr.ru
Телефон: +7 495 221-13-55