方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【ESP32】NVS暗号化キーを作成してNVSパーティションを暗号化

esp32-nvs-flash
この記事は約32分で読めます。

はじめに

方言を話す、おしゃべり猫型ロボット「ミーア」を開発中。

https://mia-cat.com

前回、こちらの記事で、AWS IoT関連の設定ファイルをLittleFS領域からNVS領域に移動するのを実装した。

ただ、このままだとセキュリティ的に脆弱なので、今回はNVS暗号化の実装を試みる。

また、現状だとFlash EncryptionとSecure BootはArduino IDEでは提供されていないし、今後も提供する予定がないとのこと。

https://github.com/espressif/arduino-esp32/issues/9233

というわけで、前準備としてPlatformIOでArduinoだけではなくESP-IDFフレームワークも扱えるように下記実装を行った。

ようやく前準備ができたところで、NVS暗号化に取り掛かる。

NVS暗号化の必要性

NVS 暗号化を使用しない場合、フラッシュ チップに物理的にアクセスできる人なら誰でも、キーと値のペアを変更、消去、または追加することができてしまう。

NVS 暗号化を有効にすると、対応する NVS 暗号化キーを知らないと、キーと値のペアを変更または追加して有効なペアとして認識されることはない。

ESP32のNVS暗号化に関しては、EspressIf公式サイトの下記に詳細記載されている。

https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/storage/nvs_encryption.html

例はこちら

https://github.com/espressif/esp-idf/tree/c7bbfaee/examples/security/flash_encryption

ただ、今回はPlatformIOフレームワークでplatform.iniファイルでESP32の定義を行っているので、そちらに合わせる形で実装を進めていく。

NVSキーパーティションの作成

まず、ESP32のNVS暗号化機能を利用するために必要な暗号化キーを保存するための専用パーティションとして、新たにNVSキーパーティションを作成する。

そうすることで、nvs_flash_init APIは自動的に生成したNVS暗号化キーをNVSキーパーティションに保存するようになる。NVS暗号化が有効になっている場合、nvs_flash_init API関数は、最初のNVSキーパーティション、すなわちdataタイプとnvs_keysサブタイプのパーティションを見つける。その後、API関数は自動的にnvsキーをそのパーティションに生成して保存する。新しいキーは、該当するキーパーティションが空の場合にのみ生成および保存される。

なので、NVSキーパーティションとして、Typeはdata, Subtypeはnvs_keysを指定する必要がある。

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/nvs_flash.html#nvs-key-partition

パーティションテーブル(partition.csv)

Plaintext
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,      0x9000, 0x5000,
nvs_keys, data, nvs_keys, 0xe000, 0x2000, #nvsキーパーティションの生成
otadata,  data, ota,      0x10000, 0x2000,
app0,     app,  ota_0,    0x20000, 0x390000,
app1,     app,  ota_1,    0x410000, 0x390000,
spiffs,   data, spiffs,   0x800000, 0x800000

sdkconfigで、NVS暗号化を有効化する

PlatformIOではCONFIG_NVS_ENCRYPTIONビルドフラグを使用できない
https://esp32.com/viewtopic.php?t=38039

Arduino IDEでは、flash encryptionとsecure bootをサポートしていない
https://esp32.com/viewtopic.php?t=10029

上記が理由で、下記記事でframworkをespidfにも対応するようにした。

sdkconfigファイルに直接設定を追加する場合、以下の設定を確認または追加する

C++
# Enable Flash encryption
CONFIG_SECURE_FLASH_ENC_ENABLED=y

# Flash encryption mode
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y  # 開発モードの場合
# CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y   # リリースモードの場合

# Enable NVS encryption
CONFIG_NVS_ENCRYPTION=y

NVS暗号化キー生成

暗号化NVSパーティションの作成

nvs_partition_gen.pyencryptコマンドを使用して、NVS暗号化キーの生成と暗号化されたNVSパーティションの作成ができる。

下記のようなコマンドを実行する

Python
python nvs_partition_gen.py encrypt sample_singlepage_blob.csv sample_encr.bin 0x3000 --keygen --keyfile sample_keys.bin

これにより、指定したCSVファイルから暗号化されたNVSパーティション(sample_encr.bin)を生成し、同時に暗号化キー(sample_keys.bin)もkeys/ディレクトリ下に生成される。

https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/storage/nvs_partition_gen.html

Python
# extra_script.py
import os
import subprocess
from SCons.Script import Import

Import("env")

# NSS暗号化キーの生成
def generate_nvs_key(source, target, env):
    print("Generating NVS encryption key...")
    key_file = "nvs_keys.bin"
    csv_file = "certificates/nvs.csv"
    bin_file = "certificates/encrypted_nvs_partition.bin"
    size = "0x5000"
    
    command = [
        "python",
        os.path.join(os.getenv('IDF_PATH'), 'components', 'nvs_flash', 'nvs_partition_generator', 'nvs_partition_gen.py'),
        "encrypt",
        csv_file,
        bin_file,
        size,
        "--keygen",
        "--keyfile", key_file
    ]
    result = subprocess.run(command, check=True)
    if result.returncode != 0:
        print("Error: NVS encryption key generation failed.")
    else:
        print("NVS encryption key generated successfully.")

# NVS暗号化キーの生成
env.AddPreAction("upload", generate_nvs_key)

このコマンドを実行して、keys/ディレクトリ下にnvs_keys.binが生成されたことを確認した。

NVSパーティションの生成と暗号化

次に、生成したNVS暗号化キーを用いて、NVSパーティションを暗号化する。encryptコマンドと—inputkeyコマンドを利用して、—inputkeyの引数として生成されたNVS暗号化キーを渡す。

Python
# NVSパーティション生成と暗号化
def generate_encrypted_nvs_partition(source, target, env):
    print("Generating and encrypting NVS partition...")
    csv_file = "certificates/nvs.csv"
    bin_file = "certificates/encrypted_nvs_partition.bin"
    key_file = "keys/nvs_keys.bin"
    size = "0x5000"

    command = [
        "python",
        os.path.join(os.getenv('IDF_PATH'), 'components', 'nvs_flash', 'nvs_partition_generator', 'nvs_partition_gen.py'),
        "encrypt",
        csv_file,
        bin_file,
        size,
        "--inputkey", key_file
    ]
    result = subprocess.run(command, check=True)
    if result.returncode != 0:
        print("Error: NVS partition generation and encryption failed.")
    else:
        print("NVS partition generated and encrypted successfully.")

# NVS暗号化キーを使用して暗号化されたNVSパーティションを生成
env.AddPreAction("upload", generate_encrypted_nvs_partition)

cryptographyパッケージのインストール

ちなみに、nvs_partition_gen.py スクリプトは cryptography パッケージに依存しているのでインストールする(warningが出た場合は)。仮想環境下の場合は下記。

ShellScript
python -m venv venv
source venv/bin/activate
pip install cryptography

requirements.txtにcryptographyを追加しておく。

Detected overlap at address: 0x8000 for fileエラー対応

ビルドしたところ、下記エラーが出現

ShellScript
esptool write_flash: error: argument <address> <filename>: Detected overlap at address: 0x8000 for file: /Users/ky/dev/clocky/clocky_platformio/.pio/build/debug/partitions.bin

パーティションテーブルでoverlapは生じていないはずなのに、と思ってググったら、同じエラーに遭遇していた人いた。

https://community.platformio.org/t/esp-boot-partitions-overlap-error-help-with-compiler-config-options/12477

2つの解決策があるとのこと

  • CONFIG_PARTITION_TABLE_OFFSET オプションを0x8000 -> 0x10000に増やす
  • CONFIG_LOG_BOOTLOADER_LEVELVERBOSE から INFO に変更

一旦、生成されている.binファイルをサイズとともに出力してみる

ShellScript
 ~/dev/clocky/clocky_platformio/.pio/build/debug   feat-nvs-encryption  ? ⍟16  ls -la *.bin                      3933  12:27:49
-rw-r--r--  1 ky  staff    36192  6  3 10:30 bootloader.bin
-rw-r--r--  1 ky  staff  2973488  6  3 10:31 firmware_debug.bin
-rw-r--r--  1 ky  staff     8192  6  3 10:30 ota_data_initial.bin
-rw-r--r--  1 ky  staff     3072  6  3 10:30 partitions.bin

verboseでビルドしているbootloader.binのサイズは、36192バイトであり、通常の0x8000 - 0x1000 = 28672バイトよりも大きい。

なので、partitions.binのオフセットがデフォルトの0x8000から始まっていると、ブートローダーの終わりと重なり、オーバーラップエラーが発生している。

CONFIG_PARTITION_TABLE_OFFSETを0x10000に変更すると、パーティションテーブルが0x10000のアドレスに配置される。この場合、ブートローダーに割り当てられるスペースは、0x10000 - 0x1000 = 60KB = 61,440バイト>36192バイトと充分量となる。

また、パーティションテーブルのオフセットの変更も必要

パーティションテーブルで設定したパーティションは下記2つの後に続く。

  1. ブートローダー: 0x1000 〜 0x8000 (通常、32KB)
  2. パーティションテーブル: 0x8000 〜 0x9000 (4KB)

というわけで、bootloader.binとpartitions.binの合計分をオフセットとしてnvsを修正する。初期のオフセットがズレるので、続きパーティションもその分ずれる。otaに関しては元々余裕を持たせていたので、そのまま。

ShellScript
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,      0x11000, 0x5000,
nvs_keys, data, nvs_keys, 0x16000, 0x2000, encrypted
otadata,  data, ota,      0x18000, 0x2000,
app0,     app,  ota_0,    0x20000, 0x390000,
app1,     app,  ota_1,    0x410000, 0x390000,
spiffs,   data, spiffs,   0x7A0000, 0x860000

上記設定に変えて再ビルドしたところ、無事ビルドを開始。

ShellScript
generate_nvs_partition(["upload"], [".pio/build/debug/firmware_debug.bin"])
Generating NVS partition with encryption...

Created encryption keys: ===>  /Users/ky/dev/clocky/clocky_platformio/keys/nvs_keys.bin

Creating NVS binary with version: V2 - Multipage Blob Support Enabled

Created NVS binary: ===> /Users/ky/dev/clocky/clocky_platformio/certificates/certs.bin
NVS partition generated successfully.
Looking for upload port...
Auto-detected: /dev/cu.wchusbserial1420
Uploading .pio/build/debug/firmware_debug.bin
esptool.py v4.5.1
Serial port /dev/cu.wchusbserial1420
Connecting....
Chip is ESP32-D0WD-V3 (revision v3.1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: d8:13:2a:25:3d:9c
Uploading stub...
Running stub...
Stub running...
Configuring flash size...

NVSパーティションのflashスクリプトの更新

前回こちらの記事で作成したflash_nvs_partition()と比較して、nvs領域のオフセットが0x09000→0x11000に変更になったので、書き込み開始位置を修正。

Python
# extra_script.py
import os
import subprocess
from SCons.Script import Import

Import("env")

def flash_nvs_partition(source, target, env):
    print("Flashing NVS partition with encryption...")

    bin_path = os.path.join(env['PROJECT_DIR'], 'certificates', 'certs.bin')
    
    command = [
        "python", "-m", "esptool",
        "--chip", "esp32",
        "--port", env['UPLOAD_PORT'],
        "--baud", str(env['UPLOAD_SPEED']),
        "write_flash", "0x11000", bin_path,
    ]
    try:
        result = subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        print("NVS partition flashed successfully.")
    except subprocess.CalledProcessError as e:
        print(f"Error: NVS partition flashing failed. Error message: {e.stderr.decode()}")

env.AddPostAction("upload", flash_nvs_partition)

NVSの初期化時に暗号化を有効にする

NVS初期化のinitNVS関数はそのままでよい。

この関数はNVSを初期化し、必要ならばNVSパーティションを消去して再初期化する。

nvs_flash_initは、NVS暗号化が有効になっている場合、自動的に暗号化を処理する。初期化時に必要なキー生成や設定を内部で行うため、追加の処理は不要。

C++
#include "nvs_helper.h"

tl::expected<void, String> NVSHelper::initNVS() {
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    if (err != ESP_OK) {
        return tl::make_unexpected("Failed to initialize NVS");
    }
    return {};
}

動作確認

ビルドしたところ下記ログが表示された

  • NVSパーティションが暗号化されている: “I (1662) nvs: NVS partition “nvs” is encrypted.”
  • NVSキーの生成: “I (1463) nvs: NVS key partition empty, generating keys”
  • フラッシュが正常に行われた: “NVS partition flashed successfully.”

無事できていそう

ShellScript

--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
20:35:01.547 > I (1789) flash_encrypt: bootloader encrypted successfully
20:35:01.547 > I (1838) flash_encrypt: partition table encrypted and loaded successfully
20:35:01.547 > I (1839) flash_encrypt: Encrypting partition 1 at offset 0x16000 (length 0x2000)...
20:35:01.547 > I (1936) flash_encrypt: Done encrypting
20:35:01.547 > I (1936) flash_encrypt: Encrypting partition 2 at offset 0x18000 (length 0x2000)...
20:35:01.547 > I (2003) flash_encrypt: Done encrypting
20:35:01.547 > I (2003) esp_image: segment 0: paddr=00020020 vaddr=3f400020 size=18518ch (1593740) map
20:35:01.547 > I (2582) esp_image: segment 1: paddr=001a51b4 vaddr=3ffb0000 size=049ech ( 18924) 
20:35:01.547 > I (2589) esp_image: segment 2: paddr=001a9ba8 vaddr=40080000 size=06470h ( 25712) 
20:35:01.547 > I (2598) esp_image: segment 3: paddr=001b0020 vaddr=400d0020 size=136bach (1272748) map
20:35:01.547 > I (3059) esp_image: segment 4: paddr=002e6bd4 vaddr=40086470 size=0f320h ( 62240) 
20:35:01.547 > I (3082) flash_encrypt: Encrypting partition 3 at offset 0x20000 (length 0x390000)...
20:35:34.033 > I (40553) flash_encrypt: Done encrypting
20:35:34.036 > E (40553) esp_image: image at 0x410000 has invalid magic byte (nothing flashed here?)
20:35:34.044 > I (40555) efuse: BURN BLOCK0
20:35:34.052 > I (40570) efuse: BURN BLOCK0 - OK (all write block bits are set)
20:35:34.058 > I (40570) flash_encrypt: Flash encryption completed
20:35:34.063 > I (40573) boot: Resetting with flash encryption enabled...
20:35:34.072 > ets Jul 29 2019 12:21:46
20:35:34.072 > 
20:35:34.072 > rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
20:35:34.078 > configsip: 0, SPIWP:0xee
20:35:34.080 > clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
20:35:34.086 > mode:DIO, clock div:2
20:35:34.089 > load:0x3fff00b8,len:10988
20:35:34.091 > ho 0 tail 12 room 4
20:35:34.091 > load:0x40078000,len:21276
20:35:34.094 > load:0x40080400,len:3836
20:35:34.097 > entry 0x4008069c
20:35:34.167 > I (56) boot: ESP-IDF 4.4.7 2nd stage bootloader
20:35:34.167 > I (56) boot: compile time 20:31:54
20:35:34.167 > I (56) boot: Multicore bootloader
20:35:34.167 > I (60) boot: chip revision: v3.1
20:35:34.167 > I (64) boot.esp32: SPI Speed      : 40MHz
20:35:34.167 > I (69) boot.esp32: SPI Mode       : DIO
20:35:34.167 > I (73) boot.esp32: SPI Flash Size : 16MB
20:35:34.167 > I (78) boot: Enabling RNG early entropy source...
20:35:34.167 > I (83) boot: Partition Table:
20:35:34.167 > I (87) boot: ## Label            Usage          Type ST Offset   Length
20:35:34.167 > I (94) boot:  0 nvs              WiFi data        01 02 00011000 00005000
20:35:34.167 > I (102) boot:  1 nvs_keys         NVS keys         01 04 00016000 00002000
20:35:34.167 > I (109) boot:  2 otadata          OTA data         01 00 00018000 00002000
20:35:34.171 > I (117) boot:  3 app0             OTA app          00 10 00020000 00390000
20:35:34.177 > I (124) boot:  4 app1             OTA app          00 11 00410000 00390000
20:35:34.185 > I (132) boot:  5 spiffs           Unknown data     01 82 007a0000 00860000
20:35:34.194 > I (139) boot: End of partition table
20:35:34.197 > I (144) esp_image: segment 0: paddr=00020020 vaddr=3f400020 size=18518ch (1593740) map
20:35:34.792 > I (748) esp_image: segment 1: paddr=001a51b4 vaddr=3ffb0000 size=049ech ( 18924) load
20:35:34.801 > I (756) esp_image: segment 2: paddr=001a9ba8 vaddr=40080000 size=06470h ( 25712) load
20:35:34.811 > I (767) esp_image: segment 3: paddr=001b0020 vaddr=400d0020 size=136bach (1272748) map
20:35:35.287 > I (1243) esp_image: segment 4: paddr=002e6bd4 vaddr=40086470 size=0f320h ( 62240) load
20:35:35.325 > I (1281) boot: Loaded app from partition at offset 0x20000
20:35:35.330 > I (1281) boot: Checking flash encryption...
20:35:35.333 > I (1281) flash_encrypt: flash encryption is enabled (3 plaintext flashes left)
20:35:35.341 > I (1288) boot: Disabling RNG early entropy source...
20:35:35.347 > I (1305) cpu_start: Multicore app
20:35:35.353 > I (1305) cpu_start: Pro cpu up.
20:35:35.355 > I (1306) cpu_start: Starting app cpu, entry point is 0x400820d8
20:35:35.364 > I (0) cpu_start: App cpu up.
20:35:35.370 > I (1324) cpu_start: Pro cpu start user code
20:35:35.373 > I (1324) cpu_start: cpu freq: 160000000
20:35:35.378 > I (1324) cpu_start: Application information:
20:35:35.384 > I (1329) cpu_start: Project name:     clocky_platformio
20:35:35.389 > I (1334) cpu_start: App version:      0.2.0-64-g45f6856-dirty
20:35:35.395 > I (1341) cpu_start: Compile time:     Jun  3 2024 20:30:50
20:35:35.400 > I (1347) cpu_start: ELF file SHA256:  792351b48ecf68fc...
20:35:35.408 > I (1353) cpu_start: ESP-IDF:          4.4.7
20:35:35.411 > I (1358) cpu_start: Min chip rev:     v0.0
20:35:35.417 > I (1363) cpu_start: Max chip rev:     v3.99 
20:35:35.422 > I (1368) cpu_start: Chip rev:         v3.1
20:35:35.425 > I (1373) heap_init: Initializing. RAM available for dynamic allocation:
20:35:35.433 > I (1380) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
20:35:35.439 > I (1386) heap_init: At 3FFBA370 len 00025C90 (151 KiB): DRAM
20:35:35.447 > I (1392) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
20:35:35.453 > I (1399) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
20:35:35.459 > I (1405) heap_init: At 40095790 len 0000A870 (42 KiB): IRAM
20:35:35.467 > I (1413) spi_flash: detected chip: generic
20:35:35.470 > I (1416) spi_flash: flash io: dio
20:35:35.475 > W (1420) flash_encrypt: Flash encryption mode is DEVELOPMENT (not secure)
20:35:35.481 > I (1430) gpio: GPIO[27]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 
20:35:35.492 > I (1438) cpu_start: Starting scheduler on PRO CPU.
20:35:35.497 > I (0) cpu_start: Starting scheduler on APP CPU.
<span style="background-color: rgb(251, 243, 219);">20:35:35.503 > I (1463) nvs: NVS key partition empty, generating keys</span>
20:35:35.685 > I (1650) nvs: NVS partition I (1654) gpio: GPIO[27]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:5 
20:35:35.697 > Starting
20:35:35.697 > After Starting - Available heap size: 252572 bytes
<span style="background-color: rgb(251, 243, 219);">20:35:35.703 > I (1662) nvs: NVS partition "nvs" is encrypted.</span>

と思ったのだが、その後、NVSを初期化してNVS領域のデータ(証明書など)を呼び出す際に、ESP_ERR_NVS_CORRUPT_KEY_PARTエラーが発生)

ShellScript
10:08:46.929 > E (1508) nvs: Failed to read NVS security cfg: [0x1117] (ESP_ERR_NVS_CORRUPT_KEY_PART)
10:08:46.938 > Failed to initialize NVS
10:08:46.943 > E (1528) nvs: Failed to read NVS security cfg: [0x1117] (ESP_ERR_NVS_CORRUPT_KEY_PART)
10:08:46.951 > failed to load app config: Failed to initialize NVS
10:08:46.954 > Guru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.
10:08:46.962 > 
10:08:46.962 > Core  1 register dump:
10:08:46.965 > PC      : 0x400d7046  PS      : 0x00060c30  A0      : 0x800e5ea6  A1      : 0x3ffc1d70  
10:08:46.973 > A2      : 0x3ffb4a20  A3      : 0x00000001  A4      : 0x0000008b  A5      : 0x00000000  
10:08:46.979 > A6      : 0x00000001  A7      : 0x0001c200  A8      : 0x3ffb5774  A9      : 0x3ffc1d50  
10:08:46.987 > A10     : 0x00000000  A11     : 0x400e44b0  A12     : 0x00000002  A13     : 0x00000000  
10:08:46.996 > A14     : 0x3ffb7034  A15     : 0x3ffb368c  SAR     : 0x0000000a  EXCCAUSE: 0x0000001c  
10:08:47.004 > EXCVADDR: 0x00000000  LBEG    : 0x400014fd  LEND    : 0x4000150d  LCOUNT  : 0xffffffff  

その後、2回目以降で起動すると、次はinvalid headerエラーが出現。

ShellScript
14:31:46.606 > rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
14:31:46.651 > invalid header: 0x26c1fbca

暗号化されていなくてplain textの状態にもかかわらず、ESP32の設定が暗号化の場合は、plain textを暗号化モードで読み取ろうとしてinvalid header errorが生じるとのこと。

こちらの対応に関しては別記事で。

なかなか一筋縄ではいかない。。。

タイトルとURLをコピーしました