Encryption downloader for STM32

In this article I would like to write about my experience of creating a bootloader for STM32 with firmware encryption. I am an individual developer, so the code below may not meet any corporate standards.

During the work process, the following tasks were set:

  • Provide user update firmware device from the SD card.
  • Ensure control of the integrity of the firmware and exclude the recording of incorrect firmware in the memory of the controller.
  • Provide firmware encryption to eliminate device cloning.

The code was written in Keil uVision using the stdperiph, fatFS and tinyAES libraries. The experimental microcontroller was STM32F103VET6, but the code can be easily adapted to another STM controller. Integrity control is provided by the CRC32 algorithm, the checksum is located in the last 4 bytes of the firmware file.

The article does not describe the creation of the project, the connection of libraries, the initialization of the periphery and other trivial steps.

First you need to decide what the bootloader is. The STM32 architecture implies flat addressing of memory when Flash memory, RAM, peripheral registers, and everything else are in the same address space. The loader is a program that starts to run when the microcontroller starts, checks whether it is necessary to update the firmware, if necessary, executes it, and starts the main program of the device. This article will describe the update mechanism from the SD card, but you can use any other source.

The firmware is encrypted using the AES128 algorithm and implemented using the tinyAES library. It consists of only two files, one with the extension .c, the other with the extension .h, therefore problems with its connection should not arise.

After creating the project, you should decide on the size of the loader and the main program. For convenience, sizes should be selected multiple of the size of the microcontroller memory page. In this example, the bootloader will occupy 64 Kb, and the main program will occupy the remaining 448 Kb. The loader will be located at the beginning of the flash memory, and the main program immediately after the loader. This should be indicated in the project settings in Keil. Our bootloader starts at address 0x80000000 (it is from it that the STM32 starts executing the code after launch) and has a size of 0x10000, we indicate this in the settings.



The main program will start at 0x08010000 and end at 0x08080000 for convenience, we will define with all addresses:

#define MAIN_PROGRAM_START_ADDRESS 0x08010000#define MAIN_PROGRAM_END_ADDRESS 0x08080000

We will also add encryption keys and an AES initialization vector to the program. These keys are best generated randomly.

staticconstuint8_t AES_FW_KEY[] = {0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF};
staticconstuint8_t AES_IV[] = {0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA};

In this example, the entire procedure for updating the firmware is built as a finite state machine. This allows you to display something on the screen during the update process, reset Watchdog and perform any other actions. For convenience, let's define with the basic states of the automaton so as not to be confused by the numbers:

#define FW_START 5#define FW_READ 1000#define FW_WRITE 2000#define FW_FINISH 10000#define FW_ERROR 100000

After initialization of the periphery, you need to check the need to upgrade the firmware. In the first state, an attempt is made to read the SD card and check for the presence of a file on it.

uint32_t t; /* Временная переменная */uint32_t fw_step; /* Индекс состояния конечного автомата */uint32_t fw_buf[512]; /* Буфер для считанного блока прошивки */uint32_t aes_buf[512]; /* Буфер для расшифрованного блока прошивки равен *//* Буферы равны размеру страницы Flash-памяти*/uint32_t idx; /* Текущий адрес в памяти */char tbuf[64]; /* Временный буфер для sprintf */ 
FATFS FS; /* Структура библиотеки fatFS - файловая система */ 
FIL F; /* Структура библиотеки fatFS - файл */case FW_READ: /* Чтение прошивки */
{
 if(f_mount(&FS, "" , 0) == FR_OK) /* Пробуем смонтировать SD-карту*/  
 { /* Проверяем, есть ли файл с прошивкой. */if(f_open(&F, "FIRMWARE.BIN", FA_READ | FA_OPEN_EXISTING) == FR_OK) 
  {
   f_lseek(&F, 0); /* Переходим в начало файла */ 
   CRC_ResetDR(); /* Сбрасываем аппаратный счетчик CRC */ 
   lcd_putstr("Обновление прошивки", 1, 0); /* Выводим сообщение на экран *//* Устанавливаем адрес чтения на начало основной программы */
   idx = MAIN_PROGRAM_START_ADDRESS; 
   fw_step = FW_READ + 10; /* Переходим к следующему состоянию */
  } else {fw_step = FW_FINISH;} /* Если файла нет - завершаем загрузчик */ 
 } else {fw_step = FW_FINISH;} /* Если нет SD-карты - завершаем загрузчик */break;
}

Now we need to check the firmware for correctness. Here, first comes the checksum verification code that runs when the file is finished reading, and then the reading itself. Perhaps you should not write like that, write in the comments what you think about it. Reading is done at 2 KB for the convenience of working with flash-memory, because the STM32F103VET6 has a memory page size of 2 KB.

case FW_READ + 10: /* Проверка корректности файла с прошивкой */ 
{
 /* В процессе показываем на экране, сколько байт считано */sprintf(tbuf, "Проверка: %d", idx - MAIN_PROGRAM_START_ADDRESS); 
 lcd_putstr(tbuf, 2, 1); 
 if (idx > MAIN_PROGRAM_END_ADDRESS) /* Если прочитаи весь файл прошивки */
 {        							 
  f_read(&F, &t, sizeof(t), &idx); /* Считываем 4 байта контрольной суммы *//* Записываем считанные 4 байта в регистр данных периферийного блока CRC */
  CRC_CalcCRC(t);  
  if(CRC_GetCRC() == 0) /* Если результат 0, то файл не поврежден */ 
  {
   /* Устанавливаем адрес записи на адрес начала основной программы */
   idx = MAIN_PROGRAM_START_ADDRESS;       
   f_lseek(&F, 0); /* Переходим в начало файла */
   fw_step = FW_READ + 20; /* Переходим к следующему состоянию */break;
  } else 
  {
   lcd_putstr("Файл поврежден", 3, 2); /* Выводим сообщение на экран */
   fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */break;
  }
 }   
 f_read(&F, &fw_buf, sizeof(fw_buf), &t); /* Считываем 2 Кб из файла в буфер */if(t != sizeof(fw_buf)) /* Если не получилось считать */ 
 {
  lcd_putstr("Ошибка чтения", 3, 2); 
  fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */break;
 }
 /* Расшифровываем считанный блок прошивки */
 AES_CBC_decrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV);
 for(t=0;t<NELEMS(aes_buf);t++) /* Записываем блок в регистр CRC */ 
 {
  CRC_CalcCRC(aes_buf[t]); /* Запись ведем по 4 байта */
 }    
 idx+=sizeof(fw_buf); /* Сдвигаем адрес на следующие 2 Кб */break;
}

Now, if the firmware is not damaged, then you need to read it again, but this time it is already recorded in Flash - memory.

case FW_READ + 20: // Flash Firmware
{
  /* В процессе показываем на экране, сколько байт записано */sprintf(tbuf, "Запись: %d", idx - MAIN_PROGRAM_START_ADDRESS);
 lcd_putstr(tbuf, 4, 2);  
 if (idx > MAIN_PROGRAM_END_ADDRESS) /* Когда записали всю прошивку */ 
 {
  lcd_putstr("Готово", 7, 3); /* Выводим сообщение на экран */
  f_unlink("FIRMWARE.BIN");  /* Удаляем файл прошивки с SD-карты */  
  fw_step = FW_FINISH; /* Завершаем загрузчик */break;
 }
 f_read(&F, &fw_buf, sizeof(fw_buf), &t); /* Считываем блок 2 Кб */if(t != sizeof(fw_buf)) /* Если не получилось считать */ 
 {
  lcd_putstr("Ошибка чтения", 3, 3); /* Выводим сообщение на экран */
  fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */break;
 }  
 /* Расшифровываем считанный блок прошивки */
 AES_CBC_decrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV);   
 FLASH_Unlock(); /* Разблокируем FLash-память на запись */ 
 FLASH_ErasePage(idx); /* Стираем страницу памяти */for(t=0;t<sizeof(aes_buf);t+=4) /* Записываем прошивку по 4 байта */ 
 {
  FLASH_ProgramWord(idx+t, aes_buf[t/4]);
 }    
 FLASH_Lock(); /* Блокируем прошивку на запись */
 idx+=sizeof(fw_buf); /* Переходим к следующей странице */break;
}

Now for beauty, we will create states for error handling and successful updating:

case FW_ERROR:
{
 /* Можно что-то сделать при ошибке обновления */break;
}
case FW_FINISH:
{
 ExecMainFW(); /* Запускаем основную программу *//* Дальнейший код выполнен не будет */break;
}

The startup function of the main ExecMainFW () program is worth considering in more detail. Here she is:

voidExecMainFW(){
 /* Устанавливаем адрес перехода на основную программу *//* Переход производится выполнением функции, адрес которой указывается вручную *//* +4 байта потому, что в самом начале расположен указатель на вектор прерывания */uint32_t jumpAddress = *(__IO uint32_t*) (MAIN_PROGRAM_START_ADDRESS + 4); 
 pFunction Jump_To_Application = (pFunction) jumpAddress;
 /*Сбрасываем всю периферию на APB1 */
 RCC->APB1RSTR = 0xFFFFFFFF; RCC->APB1RSTR = 0x0; 
/*Сбрасываем всю периферию на APB2 */ 
 RCC->APB2RSTR = 0xFFFFFFFF; RCC->APB2RSTR = 0x0; 
 RCC->APB1ENR = 0x0; /* Выключаем всю периферию на APB1 */ 
 RCC->APB2ENR = 0x0; /* Выключаем всю периферию на APB2 */
 RCC->AHBENR = 0x0; /* Выключаем всю периферию на AHB *//* Сбрасываем все источники тактования по умолчанию, переходим на HSI*/
 RCC_DeInit();  
 /* Выключаем прерывания */
 __disable_irq(); 
 /* Переносим адрес вектора прерываний */
 NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS);  
 /* Переносим адрес стэка */ 
  __set_MSP(*(__IO uint32_t*) MAIN_PROGRAM_START_ADDRESS); 
  /* Переходим в основную программу */  
  Jump_To_Application(); 
}

Immediately after launching the startup file, everything was reinitialized, so the main program should again set a pointer to the interrupt vector within its address space:

__disable_irq();
 NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS);
__enable_irq();

In the draft of the main program, you need to specify the correct addresses:



That is, in fact, the whole update procedure. The firmware is checked for correctness and encrypted, all assigned tasks are completed. In the event of a power loss during the update process, the device will, of course, become overwhelmed, but the bootloader will remain intact and the upgrade procedure can be repeated. For critical situations, you can block the page in which the loader is located via Option bytes.

However, in the case of the SD card, you can organize for yourself in the bootloader one pleasant convenience. When testing and debugging a new firmware version is completed, you can force the device itself to encrypt and unload the finished firmware onto an SD card according to some special condition (for example, a button or jumper inside). In this case, it remains only to remove the SD card from the device, insert it into the computer and put the firmware on the Internet to the joy of the users. Let's do it in the form of two more states of the finite state machine:

case FW_WRITE:
{
 if(f_mount(&FS, "" , 0) == FR_OK) /* Пробуем смонтировать SD-карту*/   
 {
  /* Пробуем создать файл */if(f_open(&F, "FIRMWARE.BIN", FA_WRITE | FA_CREATE_ALWAYS) == FR_OK)  
  {
   CRC_ResetDR(); /* Сбрасываем блок CRC *//* Устанавливаем адрес чтения на начало основной программы */
   idx = MAIN_PROGRAM_START_ADDRESS; 
   fw_step = FW_WRITE + 10; /* Переходим к следующему состоянию */    
  } else {fw_step = FW_ERROR;} /* Переходим к шагу обработки ошибки */ 
 } else {fw_step = FW_ERROR;} /* Переходим к шагу обработки ошибки */break;
}
case FW_WRITE + 10:
{
 if (idx > MAIN_PROGRAM_END_ADDRESS) /* Если выгрузили всю прошивку */ 
 {   
  t = CRC_GetCRC(); 
  f_write(&F, &t, sizeof(t), &idx); /* Дописываем в конец файла контрольную сумму */ 
  f_close(&F); /* Закрываем файл, сбрасываем кэш */ 
  fw_step = FW_FINISH; /* Завершаем зарузчик */ 
 }
 /* Считываем 2 Кб прошивки из Flash-памяти в буфер */memcpy(&fw_buf, (uint32_t *)idx, sizeof(fw_buf)); 
 for(t=0;t<NELEMS(fw_buf);t++) /* Вычисляем CRC для считанного блока */ 
 {
 CRC_CalcCRC(fw_buf[t]);
 }   
 /* Шифруем прошивку */
 AES_CBC_encrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV); 
 /* Записываем зашифрованный блок в файл */
 f_write(&F, &aes_buf, sizeof(aes_buf), &t); 
 idx+=sizeof(fw_buf); /* Сдвигаем адрес считываемого блока */break;
}

That's all I wanted to tell you. At the end of the article I would like to wish you after creating such a loader not to forget to turn on the protection of the microcontroller’s memory in Option bytes.

Links


tinyAES
FatFS

Also popular now: