はじめに
方言を話す、おしゃべり猫型ロボット「ミーア」を開発中。
前回、こちらの記事で、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://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を指定する必要がある。
パーティションテーブル(partition.csv)
# 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
ファイルに直接設定を追加する場合、以下の設定を確認または追加する
# 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.py
のencrypt
コマンドを使用して、NVS暗号化キーの生成と暗号化されたNVSパーティションの作成ができる。
下記のようなコマンドを実行する
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/ディレクトリ下に生成される。
# 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暗号化キーを渡す。
# 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が出た場合は)。仮想環境下の場合は下記。
python -m venv venv
source venv/bin/activate
pip install cryptography
requirements.txtにcryptographyを追加しておく。
Detected overlap at address: 0x8000 for fileエラー対応
ビルドしたところ、下記エラーが出現
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は生じていないはずなのに、と思ってググったら、同じエラーに遭遇していた人いた。
2つの解決策があるとのこと
CONFIG_PARTITION_TABLE_OFFSET
オプションを0x8000 -> 0x10000に増やすCONFIG_LOG_BOOTLOADER_LEVEL
VERBOSE から INFO に変更
一旦、生成されている.binファイルをサイズとともに出力してみる
~/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つの後に続く。
- ブートローダー: 0x1000 〜 0x8000 (通常、32KB)
- パーティションテーブル: 0x8000 〜 0x9000 (4KB)
というわけで、bootloader.binとpartitions.binの合計分をオフセットとしてnvsを修正する。初期のオフセットがズレるので、続きパーティションもその分ずれる。otaに関しては元々余裕を持たせていたので、そのまま。
# 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
上記設定に変えて再ビルドしたところ、無事ビルドを開始。
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に変更になったので、書き込み開始位置を修正。
# 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暗号化が有効になっている場合、自動的に暗号化を処理する。初期化時に必要なキー生成や設定を内部で行うため、追加の処理は不要。
#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.”
無事できていそう
--- 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エラーが発生)
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エラーが出現。
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が生じるとのこと。
こちらの対応に関しては別記事で。
なかなか一筋縄ではいかない。。。