Цель статьи - показать вариант построения защищенной 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 и т.п.).
Переходим к созданию сертификатов
Что нам нужно?
Создать CA (очень секретный ключ и очень публичный сертификат)
Создать ключ и сертификат сервера (подписанные CA)
Создать процесс подготовки ключей-сертификатов для клиентов.
Пп 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.
Что мы будем делать:
Подготовим схему партиций и соответствующую partition tables, с отдельной партицией fctry, в которую в дальнейшем поместим сертификаты клиента
Сгенерируем глобальную для всего проекта подпись для приложения-бинарника ESP32 и дайджест для прошивки eFuse и активации Secure Boot 2
Сгенерируем ключи для Flash encryption
Сгенерируем ключи для NVS encryption
Подготовим нашу партицию с сертификатами, зашифруем ее соответствующими ключами
Создадим обязательные шифрованные партиции для nvs key и otadata
Подпишем и зашифруем программные файлы
Прошьем в аппаратный eFuse открытый дайджест для Secure Boot 2 и ключ для Flash encryption, безвозвратно прошьем в eFuse активацию Secure Boot 2 и Flash Encryption
Прошьем бинарники с софтом, сертификатами и другими системными данными
Переведем в Release Mode, т.е. установим запрет отладки и запрет возможности доступа к шифрованным данным; оставим (несколько снижая безопасность по мнению производителя) только возможность загрузки через UART предварительно зашифрованных партиций и подписанных-зашифрованных программных файлов
Пример на языке с использования сертификатов в защищенном хранилище
Все это сделаем и на борту дивайса появится партиция с нашими сертификатами. Далее, используя наш пример кода работы с защищенной партицией и публичные примеры кода для MQTT из репозиториев производителя мы отправим сообщение на сервер.
Подготовка таблицы партиций (планирование флеш-хранилища)
Описанный выше кейс с сертификатами я включаю в схему партиций следующим образом (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-хранилища.
Создадим подпись для 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
Создаем уникальный (должен быть уникальным для каждого дивайса) ключ для Flash encryption:
espsecure.py generate_flash_encryption_key f:\dev1s12345\dev1s12345_flash_encryption_key.bin
Уникальный ключ для каждого дивайса нужен в целях исключения компрометации защиты всей сети устройств в случае компрометации одного ключа. Использование уникальных ключей обнуляет риски по известным уязвимостям ESP32.
Создадим ключи защиты nvs_key:
nvs_partition_gen.py generate-key --keyfile dev1s12345_nvs_key.bin --outdir .\dev1s12345
Создадим исходник партиции 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.
Подготовим системную партицию 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
Подпишем 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
Переходим к прошивке ключа 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
Теперь нужно прошить бинарный образ флеш-памяти:
esptool.py --port COM1 -b 460800 --after no_reset write_flash --force 0x0 .\dev1s12345\dev1s12345_merged_flash.bin
И финальный шаг - перевод устройства в 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()
В финале статьи приведу пример кода для использования сертификатов из защищенных хранилищ 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;
}
На этом все, спасибо за прочтение и комментарии.