PKI for IOT, ESP32 secure network architecture + Mosquitto SSL and Flash Encryption for storing certificates

The purpose of the article is to show an option for building a secure IoT infrastructure for a network of ESP32-based devices and to exchange experiences.

The general idea and the entire project were divided into topics:

  • deploying mosquitto SSL/TLS from a docker container

  • creating certificates for Mosquitto SSL broker and ESP32 clients

  • certificate storage architecture for ESP32 and practical security methods

  • preparing device firmware using built-in security methods for ESP32 – Secure Boot 2, Flash Encryption, NVS Encryption

So, the main differences that you need to pay attention to in this example. We use certificates to authenticate MQTT clients on ESP32. We are eliminating the client login/password infrastructure for authentication in Mosquitto. Instead of a login/password pair, Mosquitto allows you to use unique CNs in client certificates. We will store certificates on devices in a separate protected partition (Flash encryption + NVS encryption) and flash them together with the firmware of the main program (the binary of which we will also protect with a signature using Secure Boot 2). This article is not intended to delve into typical information security processes such as hardware deployment, CA compromise risk mitigation, and related security loop issues.

Let's start practicing.

The first step is to raise Mosquitto SSL

I like the deployment option using docker compose. This makes it easy to use, develop and document the infrastructure from the first trial attempts. Below I hope that readers have an idea of ​​the basic issues of administering and using Docker.

Let's prepare the mosquitto infrastructure file docker-compose.yml.

In this configuration we define port 8883 for communication with the outside world. We will also indicate the location of mosquitto storages.

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

Next, we move on to configuring mosquitto, taking into account our specifics.

The configuration file mosquitto.conf

Let's define the main important configuration parameters:

# храним все сообщения не только в памяти, но и на диске
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

This completes the basic settings.

If we have a ready-made topology of topics, then mosquitto allows us to bring beauty to the issue of isolating topics. An ACL file with rules is used for this. The file name is specified in mosquitto.conf

acl_file my_acl_file_01

This file contains patterns for topics and what turned out to be especially useful for me is the ability to specify a substitution in a pattern client id or username. An example of permission to write to a topic containing username:

pattern write my_sensor/%u/data

These ACL capabilities allow the message processing service to receive a reliable sender identifier in a topic (which, let me remind you, the broker extracts from the CN certificate) and set at the system level (i.e. without developing its own validation code) strict rules for generating topics on the device side . These advantages can largely be revealed when scaling the network using methods and solutions such as mosquitto-bridge, NiFi and/or broadcasting a stream of messages to a global streaming ecosystem (Kafka, etc.).

Let's move on to creating certificates

What do we need?

  1. Create a CA (very private key and very public certificate)

  2. Create a server key and certificate (signed by CA)

  3. Create a process for preparing certificate keys for clients.

Points 1 and 2 are performed by the administrator and are essentially a typical task of raising a CA and server. We use self-signed certificates. For a closed network this is more than enough. Let me remind you that self-signed certificates differ from authorized ones in that the chain of authorized certificates is included in public software such as browsers, etc. However, for the purposes of creating your own closed network, the use of authorized (signed by some entity, it is not at all obvious that it is friendly and permanent) chains is not only not necessary, but also, in my opinion and practical experience, harmful.

Let's create a CA

The location of all certificates related to the server infrastructure will be placed in ~/server/. For CA it will be ~/ca/.

CA key (without passphrase):

openssl genrsa -out ca/ca.key 2048

CA Certificate (30 years):

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

Let's move the CA certificate to a separate directory for further use:

cp ca/ca.crt certs/ca.crt

Let's create a broker key and certificate:

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

Let's prepare a request to 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 []:.

We will issue a server certificate based on the request:

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

Copy the certificates to the target folder, check:

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

Launch the Mosquitto service:

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

Let's check the service:

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

Let's check the connection to the service from the outside world and the server certificates:

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

The resulting tree with mosquitto files and certificates:

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

* the server folder with all its contents can be deleted as unnecessary.

File ~/mosquitto/config/mosquitto.conf from the working example:

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

Next, we need to establish the process of preparing client certificates. I’ll show you the syntax for Linux OS, and later we’ll transfer the files and work to a Windows computer. there is an IDE deployed there.

Let's go through the process of creating the first certificate. Before we begin, let's fix one rule – device identifier = folder name for client files = file names of certificates and keys = username = client Id = CN=prefixes of files with binary images of keys and device firmware. In our case, this will be a string formed using the pattern >>. For example: dev1s12345. The template was invented on the fly, solely for the purposes of this article. So, we prepare the first certificate “by hand”.

Generating the key:

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

Let's prepare a certificate request:

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 []:

These operations for a series of devices need to be automated using production scripts and/or production web services. Moreover, let me remind you that generating a key, generating a request to a CA – these functions and openssl syntax are platform independent and can be automated separately, interacting through their APIs and web services. The keys generated above for the device fall into the category of “sensitive” information in the context of information security. At the same time, these files must be available at the stage of preparing and flashing the device.

We receive a client certificate based on the request file:

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

Let's go further by copying the resulting files for our first client to the local machine:

dev1s12345.key, 
dev1s12345.crt, 
ca.crt

ESP32, preparing the infrastructure and connecting to the broker

My Windows-based workplace + Espressif IDE (ESP-IDF) + openssl.

What do we do:

  1. Let's prepare a partition scheme and the corresponding partition tables, with a separate fctry partition, into which we will later place client certificates

  2. We will generate a project-wide signature for the ESP32 binary application and a digest for eFuse firmware and Secure Boot 2 activation

  3. Let's generate keys for Flash encryption

  4. Let's generate keys for NVS encryption

  5. Let's prepare our partition with certificates and encrypt it with the appropriate keys

  6. Let's create the required encrypted partitions for nvs key and otadata

  7. We will sign and encrypt program files

  8. We will flash the open digest for Secure Boot 2 and the key for Flash encryption into the hardware eFuse, and we will permanently flash the activation of Secure Boot 2 and Flash Encryption into the eFuse

  9. We will flash binaries with software, certificates and other system data

  10. Let's switch to Release Mode, i.e. we will set a ban on debugging and a ban on the ability to access encrypted data; Let's leave (slightly reducing security according to the manufacturer) only the ability to download pre-encrypted partitions and signed-encrypted program files via UART

  11. An example in the language of using certificates in a secure storage

We’ll do all this and a partition with our certificates will appear on board the device. Next, using our example code for working with a protected partition and public examples of code for MQTT from the manufacturer’s repositories, we will send a message to the server.

  1. Preparing the partition table (planning flash storage)

I include the above case with certificates in the partition scheme as follows (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

In this set of partitions we need nvs_key and fctry.

nvs_key is a system partition for storing NVS encryption keys. The peculiarity of the ESP32 with its storage protection system is that the nvs_key for nvs partitions is protected in hardware, using flash encryption, which in turn protects all NVS storages.

  1. Let's create a signature for Secure Boot 2:

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

… And a digest of this signature, which we will need to flash the corresponding eFuse sector in the ESP32:

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

Let's prepare a folder for the first device (client):

.\dev1s12345

  1. We create a unique (must be unique for each device) key for Flash encryption:

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

A unique key for each device is needed to prevent the security of the entire network of devices from being compromised if one key is compromised. Using unique keys eliminates the risk of known ESP32 vulnerabilities.

  1. Let's create the nvs_key protection keys:

nvs_partition_gen.py generate-key --keyfile dev1s12345_nvs_key.bin --outdir .\dev1s12345
  1. Let's create the source code of the fctry partition in csv format for downloading certificates

    (file 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

Please note that in addition to certificates, we immediately added the URI of our mqtt server to the partition and included a line with the device_id device identifier to use substitutions in topic names, etc. If the topology of the topics is known in advance, then the templates or names of the topics should be included in the same file. Thus, we will 100% eliminate the dependence on a specific broker from the program code.

We convert the partition source into binary format and encrypt it with the previously created keys for 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

Please note that partitions with the NVS type are encrypted once, exclusively with their own nvs_key keys. Additionally, there is no need to mark such partitions in partition tables with the encrypted flag. And also, you cannot encrypt such partitions with keys for Flash Encryption.

  1. Let's prepare the nvs key system partition with the NVS partition protection keys:

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

Explanations for setting up the IDE.

In menuconfig or in the IDE interface – sdkconfig, specify the custom partition my_part_table.csv, then enable the Secure Boot 2 and Flash Encryption options. Disable the “sign during assembly” option. Check the NVS Encryption option (should be enabled automatically when Flash Encryption is enabled).

The initial location of the files after assembly relative to the project root folder:

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

These 4 files, after assembly and before signing and encrypting manipulations, need to be copied to our “production” disk in the .\app folder

Let's continue the preparation – now we encrypt the otadata system partition:

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

The next artifact is the 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. Let's sign the bootloader and the main application:

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

We encrypt signed bootloader and application binaries:

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

As a result, we have prepared all the binaries for flashing encryption keys, certificates and programs. The tree of the “production” section looks like this:

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

Binaries with the enc suffix are intended for flashing into separate partitions of the device. Each partition is flashed by a separate team. Sometimes, for the convenience of flashing (with one command), binaries need to be combined (“merged”). For example, like this:

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. Let's move on to flashing the flash encryption key and software signature digest.

With one command we flash all the keys and flags needed to activate secure boot 2 and flash encryption. Writing keys is an irreversible operation. At the same time, you need to pay attention to the fact that firmware of the digest and write protection of the efuse area, as well as irreversible firmware of READONLY access flags to the entire key area when secure boot 2 is activated will make secondary firmware of flash encryption keys impossible. It is recommended to flash flash encryption keys either first (before flashing secure boot 2) or in batch mode, as shown in my example.

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. Now you need to flash the binary flash memory image:

esptool.py --port COM1 -b 460800 --after no_reset write_flash --force 0x0 .\dev1s12345\dev1s12345_merged_flash.bin
  1. And the final step is to transfer the device to Release mode.

Here you will disable the transparent use of the built-in crypto provider (disable the Development node), as well as disable the built-in functions of the JTAG debugger and block overwrite protection with a network MAC. From now on, booting via UART is only possible for a pre-signed and encrypted binary.

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

It is worth noting two important factors for the further operation of the device. If replacing certificates and/or local firmware flashing during the life cycle of the device is not necessary, it is recommended to delete all production files of the device. For further firmware updates via OTA, you only need to sign the binary with the application (no encryption required). In another case, for example, if it is necessary to replace certificates, the partition will need to be re-created dev1s12345_fctry_partition_enc.bin. To do this, you will need to reuse the keys for the NVS partition of a specific device, which are located in the file .\dev1s12345\keys\dev1s12345_nvs_key.bin. It is recommended to delete all other files and keys.

To enhance device protection measures in cases where software updates are planned exclusively over the air (OTA), the manufacturer of ESP32 microcontrollers has provided (even recommends) the ability to completely disable software loading/firmware via UART. To do this, there is a convenient opportunity to call a function in runtime:

esp_efuse_disable_rom_download_mode() 
  1. At the end of the article I will give an example of code for using certificates from ESP32 secure storage.

Initializing a protected NVS partition:

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
}

Function for retrieving certificates from an open NVS storage and placing them in memory:

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;
}

That's all, thanks for reading and commenting.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *