Как стать автором
Обновить

PKI для IOT, архитектура защищенной сети ESP32 + Mosquitto SSL и Flash Encryption для хранения сертификатов

Уровень сложностиСложный
Время на прочтение16 мин
Количество просмотров2.4K

Цель статьи - показать вариант построения защищенной Iot-инфраструктуры для сети устройств на базе ESP32 и обменяться опытом. 

Общую идею и весь проект разделил на темы:

  • развертывание mosquitto SSL/TLS из docker-контейнера

  • создание сертификатов для брокера Mosquitto SSL и клиентов ESP32

  • архитектура хранилища сертификата для ESP32 и практические способы защиты

  • подготовка прошивки устройства с применение встроенных способов защиты для ESP32 - Secure Boot 2, Flash Encryption, NVS Encryption

Итак, основные отличия, на которые нужно обратить внимание в данном примере. Для аутентификации MQTT-клиентов на ESP32 применим сертификаты. Мы исключаем инфраструктуру клиентских логинов/паролей  для аутентификации в Mosquitto. Вместо пары логин/пароль Mosquitto позволяет использовать уникальные CN в клиентских сертификатах. На устройствах будем хранить сертификаты в отдельной защищенной партиции (Flash encryption + NVS encryption) и прошивать их  совместно с прошивкой основной программы (бинарник которой мы также защитим подписью с помощью Secure Boot 2). В этой статье нет цели углубиться в типовые процессы информационной безопасности, такие как применение аппаратных средств, задачи снижения рисков компрометации CA и связанные с этим вопросы организации безопасного производственного контура.

Приступим к практике.

Первым этапом поднимаем Mosquitto SSL

Мне нравится вариант развертывания с использованием docker compose. Это позволяет уже с первых пробных попыток легко использовать, развивать и документировать инфраструктуру. Ниже я ориентируюсь на то, что читатели имеют представление о базовых вопросах администрирования и использования docker.

Подготовим файл инфраструктуры mosquitto docker-compose.yml.

В этой конфигурации мы определяем порт 8883 для связи с внешним миром. А также укажем расположение хранилищ mosquitto.

version: '3.8'

services:
    mosquitto:
        image: eclipse-mosquitto:2
        ports:
            - 8883:8883
            - 9001:9001
        volumes:
            - ./mosquitto/config:/mosquitto/config/
            - ./mosquitto/data:/mosquitto/data/
            - ./mosquitto/log:/mosquitto/log/
            - ./mosquitto/certs:/mosquitto/certs/
        networks:
            - mosquitto
networks:
    mosquitto:
        name: mosquitto
        driver: bridge

Далее, переходим к конфигурированию mosquitto, с учетом нашей специфики.

Файл конфигурации mosquitto.conf

Определим основные важные конфигурационные параметры:

# храним все сообщения не только в памяти, но и на диске
persistence true
# определим нужные нам типы логов (если они отличаются от дефолтных error, warning, notice and information)
log_type
# В логах установим удобный формат времени 
log_timestamp_format %Y-%m-%dT%H:%M:%S
# Запретим анонимные подключения:
allow_anonymous false
# Установим прослушивание порта 8883 на нашем сервере:
# MQTT over TLS
listener 8883
# Установим требование клиентских сертификатов:
require_certificate true
# Исключим клоны при подключении клиентов:
use_username_as_clientid true
# Теперь нам нужно указать брокеру, что мы используем CN в качестве username:
use_identity_as_username true

# Укажем сертификаты CA, сервера, а также ключ сервера:
cafile /mosquitto/certs/ca.crt
certfile /mosquitto/certs/server.crt
keyfile /mosquitto/certs/server.key

На этом основные настройки завершены.

Если у нас есть готовая топология топиков, то mosquitto позволяет навести красоту в вопросе изоляции топиков. Для этого применяется ACL-файл с правилами. Имя файла указывается в mosquitto.conf

acl_file my_acl_file_01

В этом файле указываются паттерны для топиков и что для меня оказалось особенно полезно - возможность указывать в паттерне подстановку client id или username. Пример разрешения записи в топик, содержащий username:

pattern write my_sensor/%u/data

Эти возможности ACL позволяет сервису-обработчику сообщений получать в топике достоверный идентификатор отправителя (который, напомню, брокер извлекает из CN сертификата) и установить на системном уровне (т.е. без разработки своего программного кода валидации) строгие правила для формирования топиков на стороне устройств. Эти преимущества во многом могут раскрыться при масштабировании сети с применением способов и решений типа mosquitto-bridge, NiFi и/или трансляции потока сообщений в глобальную стрим-экосистему (Kafka и т.п.). 

Переходим к созданию сертификатов

Что нам нужно? 

  1. Создать CA (очень секретный ключ и очень публичный сертификат)

  2. Создать ключ и сертификат сервера (подписанные CA)

  3. Создать процесс подготовки ключей-сертификатов для клиентов.

Пп 1 и 2 выполняются руками администратора и являются по сути типовой задачей поднятия CA и сервера. Мы используем самоподписанные сертификаты. Для закрытой сети этого более чем достаточно. Напомню, что самоподписанные сертификаты отличаются от авторизованных тем, что цепочка авторизованных сертификатов включается в публичное ПО, такое как браузеры и т.п. Однако, для целей создания своей закрытой сети использование авторизованных (подписанных каким-то субъектом, совершенно неочевидно, что дружественным и постоянным) цепочек не только не обязательно, но и на мой взгляд и практический опыт - вредно. 

Создадим CA

Расположение всех сертификатов, относящихся к серверной инфраструктуре положим в ~/server/. Для CA будет~/ca/.

Ключ CA (без passphrase):

openssl genrsa -out ca/ca.key 2048

Сертификат CA (на 30 лет):

openssl req -new -x509 -days 10950 -key ca/ca.key -out ca/ca.crt
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:RU
State or Province Name (full name) [Some-State]:Moscow
Locality Name (eg, city) []:Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Leo4
Organizational Unit Name (eg, section) []:iot
Common Name (e.g. server FQDN or YOUR name) []:iot.leo4.ru
Email Address []:iot@leo4.ru

Перенесем сертификат CA в отдельный каталог для дальнейшего использования:

cp ca/ca.crt certs/ca.crt

Сделаем ключ и сертификат брокера:

mkdir server
sudo openssl genrsa -out server/server.key 2048

Подготовим запрос в CA:

oleg@leo4broker2:sudo openssl req -new -out server/server.csr -key server/server.key
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:RU
State or Province Name (full name) [Some-State]:Moscow
Locality Name (eg, city) []:Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Leo4
Organizational Unit Name (eg, section) []:mqtt-broker
Common Name (e.g. server FQDN or YOUR name) []:iot.leo4.ru
Email Address []:iot@leo4.ru

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:.
An optional company name []:.

Выдадим сертификат сервера на основе запроса:

oleg@leo4broker2:~$ sudo openssl x509 -req -in server/server.csr -CA ca/ca.crt -CAkey ca/ca.k
ey -CAcreateserial -out server/server.crt -days 10950
Certificate request self-signature ok
subject=C = RU, ST = Moscow, L = Moscow, O = Leo4, OU = mqtt-broker, CN = iot.leo4.ru, emailAddress = iot@leo4.ru

Копируем сертификаты в целевую папку, проверяем:

oleg@mqtt:~/server$ ls
server.crt  server.csr  server.key
oleg@mqtt:~$ cp ca/ca.crt mosquitto/certs/
oleg@mqtt:~$ cp server/server.crt mosquitto/certs/
oleg@mqtt:~$ cp server/server.key mosquitto/certs/
cp: cannot open 'server/server.key' for reading: Permission denied
oleg@mqtt:~$ sudo cp server/server.key mosquitto/certs/
oleg@mqtt:~$ cd mosquitto
oleg@mqtt:~/mosquitto$ ls
certs  config  data  etc  log
oleg@mqtt:~/mosquitto$ cd certs
oleg@mqtt:~/mosquitto/certs$ ls
ca.crt  server.crt  server.key

Запускаем сервис Mosquitto:

oleg@mqtt:~$ sudo docker compose up -d
[+] Running 4/4
 ✔ mosquitto 3 layers [⣿⣿⣿]      0B/0B      Pulled                          4.5s
   ✔ c926b61bad3b Pull complete                                              0.6s
   ✔ a87e6a8c3b38 Pull complete                                              0.6s
   ✔ 4e2ff55c815a Pull complete                                              0.5s
[+] Running 1/2 Network mosquitto           Created                          1.0s
 ✔ Container oleg-mosquitto-1  Started

Проверим работу сервиса:

oleg@mqtt:~$ sudo docker compose ps
NAME               IMAGE                 COMMAND                  SERVICE     CREATED          STATUS          PORTS
oleg-mosquitto-1   eclipse-mosquitto:2   "/docker-entrypoint...."   mosquitto   43 minutes ago   Up 12 minutes   0.0.0.0:8883->8883/tcp, :::8883->8883/tcp, 1883/tcp, 0.0.0.0:9001->9001/tcp, :::9001->9001/tcp

Проверим подключение к сервису из внешнего мира и сертификаты сервера:

D:\OpenSSL-Win64\bin>openssl s_client -connect iot.leo4.ru:8883
CONNECTED(000001E0)
depth=1 C = RU, ST = Moscow, L = Moscow, O = Leo4, OU = IT, CN = iot.leo4.ru, emailAddress = iot@leo4.ru
verify error:num=19:self-signed certificate in certificate chain
verify return:1
depth=1 C = RU, ST = Moscow, L = Moscow, O = Leo4, OU = IT, CN = iot.leo4.ru, emailAddress = iot@leo4.ru
verify return:1
depth=0 C = RU, ST = Moscow, L = Moscow, O = Leo4, OU = mqtt, CN = iot.leo4.ru, emailAddress = iot@leo4.ru
verify return:1
---
Certificate chain
 0 s:C = RU, ST = Moscow, L = Moscow, O = Leo4, OU = mqtt, CN = iot.leo4.ru, emailAddress = iot@leo4.ru
   i:C = RU, ST = Moscow, L = Moscow, O = Leo4, OU = IT, CN = iot.leo4.ru, emailAddress = iot@leo4.ru
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Jan 25 13:56:50 2024 GMT; NotAfter: Jan 17 13:56:50 2054 GMT
 1 s:C = RU, ST = Moscow, L = Moscow, O = Leo4, OU = IT, CN = iot.leo4.ru, emailAddress = iot@leo4.ru
   i:C = RU, ST = Moscow, L = Moscow, O = Leo4, OU = IT, CN = iot.leo4.ru, emailAddress = iot@leo4.ru
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Jan 25 13:50:25 2024 GMT; NotAfter: Jan 17 13:50:25 2054 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
..

Итоговое дерево с файлами mosquitto и сертификатами:

oleg@mqtt:~$ tree .
.
├── ca
│   ├── ca.crt
│   └── ca.key
├── docker-compose.yml
├── mosquitto
│   ├── certs
│   │   ├── ca.crt
│   │   ├── server.crt
│   │   └── server.key
│   ├── config
│   │   └── mosquitto.conf
│   ├── data
│   │   └── mosquitto.db
│   ├── etc
│   └── log
│       └── mosquitto.log
└── server
    ├── server.crt
    ├── server.csr
    └── server.key

8 directories, 12 files

* папку server со всем содержимым можно удалить за ненадобностью.

Файл ~/mosquitto/config/mosquitto.conf из работающего примера:

persistence true
persistence_location /mosquitto/data/
#logs
log_type subscribe
log_type unsubscribe
log_type websockets
log_type error
log_type warning
log_type notice
log_type information
log_dest file /mosquitto/log/mosquitto.log
connection_messages true
log_timestamp true
log_timestamp_format %Y-%m-%dT%H:%M:%S
#log to the console
log_dest stdout
#log to the topic $SYS/broker/log/#
log_dest topic

#password_file /mosquitto/passwd_file
allow_anonymous false

# MQTT over WebSockets
listener 9001 0.0.0.0
protocol websockets

# MQTT over TLS
listener 8883
require_certificate true
use_identity_as_username true
use_username_as_clientid true

cafile /mosquitto/certs/ca.crt
certfile /mosquitto/certs/server.crt
keyfile /mosquitto/certs/server.key

Далее, нам нужно наладить процесс подготовки клиентских сертификатов. Покажу синтаксис для ОС Линукс, а в дальнейшем перенесем файлы и работу на компьютер Windows т.к. там развернуто IDE. 

Пройдем путь создания первого сертификата. Перед началом зафиксируем одно правило - идентификатор устройства = имя папки для файлов клиента = имена файлов сертификатов и ключей = username = client Id = CN=префиксы файлов с бинарными образами ключей и прошивками устройства. В нашем случае это будет строка, сформированная по шаблону <dev><порядковый номер><s><случайные 5 цифр>. Например: dev1s12345. Шаблон придуман на ходу, исключительно для целей данной статьи. Итак, готовим первый сертификат “ручками”.

Генерируем ключ:

oleg@mqtt:~$ mkdir dev1s12345
oleg@mqtt:~$ cd dev1s12345
oleg@mqtt:~/dev1s12345$ openssl genrsa -out dev1s12345.key 2048

Подготовим запрос на сертификат:

oleg@mqtt:~/dev1s12345$ openssl req -new -out  dev1s12345.csr -key  dev1s12345.key
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:RU
State or Province Name (full name) [Some-State]:Moscow
Locality Name (eg, city) []:Moscow
Organization Name (eg, company) [Internet Widgits Pty Ltd]:IT
Organizational Unit Name (eg, section) []:IT
Common Name (e.g. server FQDN or YOUR name) []:dev1s12345
Email Address []:device@leo4.ru

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Эти операции для серии устройств нужно автоматизировать с помощью производственных скриптов и/или производственных веб-сервисов. Более того, напомню, что генерация ключа, формирование запроса к CA  - эти функции и синтаксис openssl являются платформенно независимыми и могут быть автоматизированы отдельно, взаимодействуя через свои АПИ и веб-сервисы. Сгенерированные выше ключи для устройства попадают в разряд “чувствительной” информации в контексте информационной безопасности. В тоже время, эти файлы должны быть доступны на стадии подготовки и прошивки устройства.

Получаем сертификат клиента на основе файла-запроса:

oleg@mqtt:~/dev1s12345$ sudo openssl x509 -req -in dev1s12345.csr -CA ~/ca/ca.crt -CAkey ~/ca/ca.key -CAcreateserial -ou
t dev1s12345.crt -days 10950
Certificate request self-signature ok
subject=C = RU, ST = Moscow, L = Moscow, O = IT, OU = IT, CN = dev1s12345, emailAddress = device@leo4.ru

Пойдем дальше, скопировав на локальную машину полученные файлы для  нашего первого клиента:

dev1s12345.key, 
dev1s12345.crt, 
ca.crt

ESP32, готовим инфраструктуру и подключаемся к брокеру

Мое рабочее место на базе Windows + Espressif IDE (ESP-IDF) + openssl. 

Что мы будем делать:

  1. Подготовим схему партиций и соответствующую partition tables, с отдельной партицией fctry, в которую в дальнейшем поместим сертификаты клиента

  2. Сгенерируем глобальную для всего проекта подпись для приложения-бинарника ESP32 и дайджест для прошивки eFuse и активации Secure Boot 2

  3. Сгенерируем ключи для Flash encryption

  4. Сгенерируем ключи для NVS encryption

  5. Подготовим нашу партицию с сертификатами, зашифруем ее соответствующими ключами 

  6. Создадим обязательные шифрованные партиции для nvs key и otadata

  7. Подпишем и зашифруем программные файлы

  8. Прошьем в аппаратный eFuse открытый дайджест для Secure Boot 2 и ключ для Flash encryption, безвозвратно прошьем в eFuse активацию Secure Boot 2 и Flash Encryption 

  9. Прошьем бинарники с софтом,  сертификатами и другими системными данными 

  10. Переведем в Release Mode, т.е. установим запрет отладки и запрет возможности доступа к шифрованным данным; оставим (несколько снижая безопасность по мнению производителя) только возможность загрузки через UART предварительно зашифрованных партиций и подписанных-зашифрованных программных файлов

  11. Пример на языке с использования сертификатов в защищенном хранилище

Все это сделаем и на борту дивайса появится партиция с нашими сертификатами. Далее, используя наш пример кода работы с защищенной партицией и публичные примеры кода для MQTT из репозиториев производителя мы отправим сообщение на сервер.

  1. Подготовка таблицы партиций (планирование флеш-хранилища)

Описанный выше кейс с сертификатами я включаю в схему партиций следующим образом (my_part_table.csv):

# Name, Type, SubType, Offset, Size, Flags
nvs,      data, nvs,0x11000,  0x4000,
otadata,  data, ota,       ,  0x2000, encrypted
phy_init, data, phy,       ,  0x1000,
factory,  app,  factory,   ,  0x110000,
ota_0,    app,  ota_0,     , 0x110000,
ota_1,    app,  ota_1,     , 0x110000,
fctry,    data, nvs,       , 0x10000,
db1,      data, nvs,       , 512K,
nvs_key,  data, nvs_keys,  , 0x1000, encrypted

В этом наборе партиций нам нужны nvs_key и fctry

nvs_key - это системная партиция для хранения ключей шифрования NVS. Особенность ESP32 с ее системой защиты хранилищ в том, что аппаратно, с помощью flash encryption защищается ключ nvs_key для nvs-партиций, который, в свою очередь защищает все NVS-хранилища.

  1. Создадим подпись для Secure Boot 2:

espsecure.py generate_signing_key --version 2 --scheme rsa3072 f:\global_secret\secure_boot_signing_key.pem

... И дайджест этой подписи, который нам потребуется для прошивки соответствующего сектора eFuse в ESP32:

espsecure.py digest_sbv2_public_key --keyfile f:\global_secret\secure_boot_signing_key.pem --output f:\secure_boot2\digest.bin

Подготовим папку для первого дивайса (клиента):

.\dev1s12345

  1. Создаем уникальный (должен быть уникальным для каждого дивайса) ключ для Flash encryption:

espsecure.py generate_flash_encryption_key f:\dev1s12345\dev1s12345_flash_encryption_key.bin

Уникальный ключ для каждого дивайса нужен в целях исключения компрометации защиты всей сети устройств в случае компрометации одного ключа. Использование уникальных ключей обнуляет риски по известным уязвимостям ESP32.

  1. Создадим ключи защиты nvs_key:

nvs_partition_gen.py generate-key --keyfile dev1s12345_nvs_key.bin --outdir .\dev1s12345
  1. Создадим исходник партиции fctry  в формате csv для загрузки сертификатов

    (файл dev1s12345_fctry_partition.csv):

key,type,encoding,value
mqtt_ns,namespace,,
server_uri,data,string,mqtts://iot.leo4.ru:8883
device_id,data,string,dev1s12345
ca_cert,file,binary,ca.crt
cert,file,binary,dev1s12345.crt
priv_key,file,binary,dev1s12345.key

Обратите внимание, что в партицию мы, помимо сертификатов, сразу добавили URI нашего mqtt-сервера и включили строку с идентификатором дивайса device_id для использования подстановок в имена топиков и т.п. Если заранее известна топология топиков, то шаблоны или имена топиков следует включить в этот же файл. Таким образом, мы на 100% исключим из кода программы зависимость от конкретного брокера. 

Конвертируем исходник партиции в бинарный формат и шифруем с ранее созданными ключами для nvs_key:

nvs_partition_gen.py encrypt --inputkey .\dev1s12345\keys\dev1s12345_nvs_key.bin .\dev1s12345\dev1s12345_fctry_partition.csv .\dev1s12345\dev1s12345_fctry_partition_enc.bin 0x10000

Обращаю внимание, что партиции с типом NVS шифруются однократно, исключительно “своими” ключами nvs_key. Дополнительно помечать такие партиции в partition tables флагом encrypted не нужно. А также, нельзя шифровать такие партиции ключами для Flash Encryption.

  1. Подготовим системную партицию nvs key с ключами защиты NVS партиций:

espsecure.py --keyfile .\dev1s12345\dev1s12345_flash_encryption_key.bin --address 0x3e0000 --output .\dev1s12345\dev1s12345_nvs_key_partition_enc.bin .\dev1s12345\keys\dev1s12345_nvs_key.bin

Пояснения по настройке IDE.

В menuconfig или в интерфейсе IDE - sdkconfig укажем кастомную партицию my_part_table.csv, далее - включаем опции Secure Boot 2 и Flash Encryption. Отключаем опцию “подписывать при сборке”. Проверяем опцию NVS Encryption (должна быть включена автоматически при включении Flash Encryption).

Исходное расположение файлов после сборки относительно корневой папки проекта:

.\build\myapp.bin
.\build\bootloader\bootloader.bin
.\build\partition_table\partition-table.bin
.build\ota_data_initial.bin

Эти 4 файла после сборки и перед манипуляциями по подписанию и шифрованию нужно скопировать на наш “производственный” диск в папку .\app

Продолжим подготовку - теперь шифруем системную партиции otadata:

espsecure.py --keyfile .\dev1s12345\dev1s12345_flash_encryption_key.bin --address 0x15000 --output .\dev1s12345\dev1s12345_otadata_enc.bin .\app\ota_data_initial.bin

Следующий артефакт - таблица партиций (partition table):

espsecure.py --keyfile .\dev1s12345\dev1s12345_flash_encryption_key.bin --address 0x10000 --output .\dev1s12345\dev1s12345_partition_table_enc.bin .\app\partition-table.bin
  1. Подпишем bootloader и основное приложение:

espsecure.py sign_data --version 2 --keyfile .\global_secret\secure_boot_signing_key.pem --output .\app\bootloader_signed.bin .\app\bootloader.bin

espsecure.py sign_data --version 2 --keyfile .\global_secret\secure_boot_signing_key.pem --output .\app\myapp_signed.bin .\app\myapp.bin

Шифруем подписанные бинарники bootloader и приложения:

espsecure.py --keyfile .\dev1s12345\dev1s12345_flash_encryption_key.bin --address 0x1000 --output .\dev1s12345\dev1s12345_bootloader_enc.bin .\app\bootloader_signed.bin

espsecure.py --keyfile .\dev1s12345\dev1s12345_flash_encryption_key.bin --address 0x20000 --output .\dev1s12345\dev1s12345_myapp_enc.bin .\app\myapp_signed.bin

В результате, у нас подготовлены все бинарники для прошивки ключей шифрования, сертификатов и программы. Дерево “производственного” раздела выглядит так:

F:
├── secure_boot2
│   └── digest.bin
├── global_secret
│   ├── secure_boot_signing_key.pem
│   
├── app
│   ├── bootloader.bin
│   ├── ota_data_initial.bin
│   ├── myapp.bin
│   ├── partition-table.bin
│   ├── bootloader_signed.bin
│   └── myapp_signed.bin
├── ca
│   └── ca.crt
├── prepare_stage1.cmd  
├── burn_stage1.cmd
├── my_part_table.csv
├── dev1s12345
    ├── keys
    │   └── dev1s12345_nvs_key.bin
    ├── mqtt
    │	  ├── dev1s12345.key
    │	  └── dev1s12345.crt
    ├── dev1s12345_flash_encryption_key.bin
    ├── dev1s12345_fctry_partition.csv
    ├── dev1s12345_fctry_partition_enc.bin
    ├── dev1s12345_nvs_key_partition_enc.bin
    ├── dev1s12345_otadata_enc.bin
    ├── dev1s12345_bootloader_enc.bin
    ├── dev1s12345_myapp_enc.bin
    └── dev1s12345_partition_table_enc.bin

Бинарники с суффиксом enc предназначены для прошивки в отдельные партиции устройства. Каждая партиция прошивается отдельной командой. Иногда, для удобства прошивки (одной командой) бинарники нужно объединить (“мерджить”). Например, вот так:

esptool.py --chip ESP32 merge_bin -o^
.\dev1s12345\dev1s12345_merged_flash.bin --flash_mode dio --flash_size 4MB^
0x1000 .\dev1s12345\dev1s12345_bootloader_enc.bin^
0x10000 .\dev1s12345\dev1s12345_partition_table_enc.bin^
0x20000 .\dev1s12345\dev1s12345_myapp_enc.bin^
0x15000 .\dev1s12345\dev1s12345_otadata_enc.bin^
0x3e0000 .\dev1s12345\dev1s12345_nvs_key_partition_enc.bin^
0x350000 .\dev1s12345\dev1s12345_fctry_partition_enc.bin
  1. Переходим к прошивке ключа flash encryption и дайджеста подписи ПО.

Одной командой прошиваем все ключи и флаги, нужные для активации secure boot 2 и flash encryption. Запись ключей является необратимой операцией. При этом, нужно обратить внимание на то, что прошивка дайджеста и защита области efuse от записи, а также необратимая прошивка флагов доступа READONLY ко всей области ключей при активации secure boot 2 сделает невозможной вторичную прошивку ключей flash encryption. Рекомендуется прошивать ключи flash encryption либо в первую очередь (до прошивки secure boot 2), либо в пакетном режиме, как показано в моем примере.

espefuse.py --port COM1 burn_key flash_encryption .\dev1s12345\dev1s12345_flash_encryption_key.bin^
burn_key secure_boot_v2 .\secure_boot2\\digest.bin^
burn_efuse FLASH_CRYPT_CNT 127^
burn_efuse FLASH_CRYPT_CONFIG 0xF^
burn_efuse ABS_DONE_1
  1. Теперь нужно прошить бинарный образ флеш-памяти:

esptool.py --port COM1 -b 460800 --after no_reset write_flash --force 0x0 .\dev1s12345\dev1s12345_merged_flash.bin
  1. И финальный шаг - перевод устройства в Release mode.

Здесь произойдет отключение прозрачного использования встроенного криптопровайдера (отключение Development node), а также отключение встроенных функций отладчика JTAG и защита от перезаписи блока с сетевым MAC. С этого момента загрузка через UART возможна только для предварительно подписанного и зашифрованного бинарника.

espefuse.py --port COM3 burn_efuse DISABLE_DL_ENCRYPT 0x1 burn_efuse DISABLE_DL_DECRYPT 0x1 burn_efuse DISABLE_DL_CACHE 0x1 burn_efuse JTAG_DISABLE 0x1 write_protect_efuse MAC write_protect_efuse RD_DIS write_protect_efuse DISABLE_DL_ENCRYPT

Стоит отметить два важных фактора для дальнейшей эксплуатации устройства. Если замена сертификатов и/или локальная перепрошивка ПО в течение жизненного цикла устройства не будет необходима, то рекомендуется удалить все производственные файлы устройства. Для дальнейших обновлений прошивки ПО посредством OTA потребуется только подписывать бинарник с приложением (шифровать не требуется). В другом случае, например, при необходимости замены сертификатов потребуется пересоздание партиции dev1s12345_fctry_partition_enc.bin. Для этого нужно будет вновь использовать ключи для NVS-партиции конкретного устройства, которые находятся в файле .\dev1s12345\keys\dev1s12345_nvs_key.bin. Все остальные файлы и ключи рекомендуется удалить. 

Для усиления мер защиты устройства в случаях, когда обновление ПО планируется исключительно по “воздуху” (OTA) производитель микроконтроллеров ESP32 предусмотрел (даже рекомендует) и возможность полного отключения загрузки/прошивки ПО через UART. Для этого есть удобная возможность в рантайме вызвать функцию:

esp_efuse_disable_rom_download_mode() 
  1. В финале статьи приведу пример кода для использования сертификатов из защищенных хранилищ ESP32.

Инициализация защищенной NVS-партиции:

static esp_err_t custom_nvs_part_init(const char *name)
{
#if CONFIG_NVS_ENCRYPTION
    esp_err_t ret = ESP_FAIL;
    const esp_partition_t *key_part = esp_partition_find_first(
                                          ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS_KEYS, NULL);
    if (key_part == NULL) {
        ESP_LOGE(TAG, "CONFIG_NVS_ENCRYPTION is enabled, but no partition with subtype nvs_keys found in the partition table.");
        return ret;
    }
    nvs_sec_cfg_t cfg = {};
    ret = nvs_flash_read_security_cfg(key_part, &cfg);
    if (ret != ESP_OK) {
        /* We shall not generate keys here as that must have been done in default NVS partition initialization case */
        ESP_LOGE(TAG, "Failed to read NVS security cfg: [0x%02X] (%s)", ret, esp_err_to_name(ret));
        return ret;
    }
    ret = nvs_flash_secure_init_partition(name, &cfg);
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "NVS partition \"%s\" is encrypted.", name);
    }else ESP_LOGE(TAG, "Failed to init NVS partition: [0x%02X] (%s)", ret, esp_err_to_name(ret));
    return ret;
#else
    return nvs_flash_init_partition(name);
#endif
}

Функция извлечения сертификатов из открытого NVS-хранилища и размещения их в памяти:

int alloc_and_read_from_nvs(nvs_handle handle, const char *key, char **value)
{
    size_t required_size = 0;
    if ((nvs_get_blob(handle, key, NULL, &required_size)) != ESP_OK) return -1;
    *value = calloc(1, required_size + 1);  /* The extra byte is for the NULL termination */
    if (*value) {
        nvs_get_blob(handle, key, *value, &required_size);
        return 0;
    }
    return -1;
}

На этом все, спасибо за прочтение и комментарии.

Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии20

Публикации

Истории

Работа

Ближайшие события

One day offer от ВСК
Дата16 – 17 мая
Время09:00 – 18:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область