Welcome to telecotele.com » Projects

ESP32-C3のセキュリティ機能を試す(Arduino編)

ESP32-C3で利用可能な「セキュアブート」「フラッシュ暗号化」「NVS暗号化」を実機で、Arduinoを使って試します。

tags: esp WORKING

ESP32-C3のセキュリティ機能を試す(Arduino編)のカバー画像

ESP32-C3のセキュリティ機能を試す(概要・予習編)の続きです。 前回は、ESP32-C3で利用可能な「セキュアブート」「フラッシュ暗号化」「NVS暗号化」の概要をまとめ、QEMUでESP-IDFフレームワークのみを使って予習しました。

今回はArduinoフレームワークのアプリケーションを開発し、実機で動作させ各種セキュリティ機能も有効にします。

開発方法

そもそも、ESP32-C3をはじめESP32系のデバイスで動くようなArduinoフレームワークのアプリケーションを開発する、とは? これはespressif/arduino-esp32を使って開発を進めていくわけですが、具体的な方法としては次の3つ1の方法A,B,Cがあります。

A: Arduino IDEを利用

まず、Arduino IDEを使って開発するという方法があります。 ESP32系でArduinoのアプリケーションを開発するといえばだいたいこれ、9割方……いや103万%、120%この方法を使うことになるでしょう。 むしろこれ以外に方法あるんですか?というレベル。

下記のEspressifによるインストール手順でもArduino IDEを使う前提となっています。

Installing - - — Arduino ESP32 latest documentation
https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html

この手順によると、まずArduino IDE自体をインストールし、ボードマネージャーに特定のURLを書く、そしてボードとしてesp32をインストールする、となっています。 ボードマネージャーが使えない(または使いたくない)場合は、明示的にesp32ボードをインストールするPythonスクリプトも提供されています。 簡単で良いですね。

こうして開発したArduinoのアプリケーション内部では、ESP-IDFのAPIを呼び出して使っています。 でもESP-IDFのインストールってしましたっけ……? 実はこのArduino IDEを使う方法だと、ESP-IDFのインストールは不要です。 たまたま別でESP-IDFをインストールしていることもあるかも知れませんが、それは関係ありません。

では、どうやってESP-IDFのAPIを使っているんでしょうか? また、2ndステージブートローダもどこかから用意する必要がありそうですが、それはどこから? これは、esp32ボードをインストールする際にESP-IDFの静的ライブラリおよびビルド済みの2ndステージブートローダが取得されており、それがArduino IDEでビルドされたときにリンクされるようになっています。

ボードマネージャーに登録するURL(Stable版)https://espressif.github.io/arduino-esp32/package_esp32_index.jsonを確認すると、“esp32-arduino-libs”という名前のライブラリを取得する2ことが分かります。

$ wget https://espressif.github.io/arduino-esp32/package_esp32_index.json

$ cat package_esp32_index.json | jq '.packages[0].platforms[0].toolsDependencies[] | select(.name == "esp32-ar
duino-libs")'
{
  "packager": "esp32",
  "name": "esp32-arduino-libs",
  "version": "idf-release_v5.3-489d7a2b-v1"
}

$ cat package_esp32_index.json | jq '.packages[0].tools[] | select(.name == "esp32-arduino-libs" and .version == "idf-release_v5.3-489d7a2b-v1").systems[0]'
{
  "host": "i686-mingw32",
  "url": "https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.3/esp32-arduino-libs-idf-release_v5.3-489d7a2b-v1.zip",
  "archiveFileName": "esp32-arduino-libs-idf-release_v5.3-489d7a2b-v1.zip",
  "checksum": "SHA-256:489012502218a7d30f6c312764bc8d10830a51e1db29558f15181c68373d0095",
  "size": "341414090"
}

この中身に静的ライブラリおよびビルド済みの2ndステージブートローダが含まれています。

$ cd esp32-arduino-libs-idf-release_v5.3-489d7a2b-v1/esp32-arduino-libs/esp32c3/

$ file lib/libnvs_*
lib/libnvs_flash.a:        current ar archive
lib/libnvs_sec_provider.a: current ar archive

$ file bin/*
bin/bootloader_dio_40m.elf: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped
bin/bootloader_dio_80m.elf: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped
bin/bootloader_qio_40m.elf: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped
bin/bootloader_qio_80m.elf: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped

ESP-IDFの面倒も見てくれるなんて、やっぱり簡単で良いですね。

注意点

ところが、今回のようにセキュリティ機能を有効にして使おうと思うと注意が必要です。

まずはセキュアブートについて。 eFuseにSECURE_BOOT_ENを設定して公開鍵のダイジェストも書いた、とします。 2ndステージブートローダやアプリケーションへの署名は……これ自体も難しそうですが、Arduino IDEでは未署名のバイナリを作成して外部で署名して書き込んだ、ことにしておきます。 これで実行!しかし、2ndステージブートローダがアプリケーションの署名を検証するか否かは、2ndステージブートローダのビルド時に決定3されます。 esp32-arduino-libsから取得された2ndステージブートローダをそのまま使うなら、アプリケーションの署名は検証されないでしょう。 じゃあ2ndステージブートローダだけは独自にビルドすれば問題ない?といえるかも知れませんが、この辺は未確認です。 (OTA Updateを使う場合はそこでも署名を検証するんじゃないか、ということは結局アプリケーション側でも検証の仕組みを導入することになるんじゃないかという気はしますが……。)

また、NVS暗号化についても注意が必要です。 NVS領域、特にデフォルトパーティションを使う想定ならNVS暗号化が有効か否かを問わずnvs_flash_init()を呼ぶことになります。 実際、arduino-esp32でも初期化時に呼んでいるようです。 nvs_flash_init()の内部では、NVS暗号化が無効ならnvs_flash_init_partition()を、NVS暗号化が有効ならnvs_flash_secure_init()を呼びます。 しかし、この「NVS暗号化が無効なら」「有効なら」という判定も、先ほどのセキュアブートと同様に(静的ライブラリの)ビルド時4です。 esp32-arduino-libsのデフォルト設定ではNVS暗号化を無効としているため、これだとNVS暗号化が使えません。 独自にnvs_flash_deinit()を呼び、改めてnvs_flash_secure_init()を呼び出せばいけるんじゃないか……という気はしますが、この辺は未検証です。

ということで、セキュアブートとNVS暗号化を使う場合は、このままだと難がありそうです。

なお、フラッシュ暗号化については、おそらくeFuseの設定だけで機能が使えるようになると思われます。 多少ログ出力は変わるかも知れない、また今後もこの動作が保証されているか?は分かりませんが……。

B: Library Builder

前述の「難」は、esp32-arduino-libsからビルド済みの2ndステージブートローダや静的ライブラリを取得していたために起こっていました。 ふむ……では、そのライブラリの設定を変えてビルドし直せば良いのでは?(なろう系)

ちゃんと独自にビルドできる仕組みも用意されています。

Library Builder - - — Arduino ESP32 latest documentation
https://docs.espressif.com/projects/arduino-esp32/en/latest/lib_builder.html

これでビルドして既存のesp32-arduino-libsと置き換えればOKです。

ただ、これだとすべてのプロジェクトで同じ設定のesp32-arduino-libsを使うことになります。 別にそれで問題ない、すべてのプロジェクトでセキュリティ機能を有効にするんだよ、というなら良いですがそうでもないなら面倒なことになりそうです。

C: ESP-IDFに組み込む

これまでの方法はArduino IDEを使ってどうにかする……といったものでした。 別の方法として、ESP-IDFにコンポーネントとしてArduinoフレームワークを組み込む方法もあります。

Arduino as an ESP-IDF component - - — Arduino ESP32 latest documentation
https://docs.espressif.com/projects/arduino-esp32/en/latest/esp-idf_component.html

これだとプロジェクトごとに導入するか否かを決定でき、もちろんプロジェクトごとなので設定も独立です。

結局どれが良いか

方法としてはこんなところでしょうか。 これ以外の方法は絶対無い、とは言いませんが自分は上記に挙げたいずれかの方法を採用しようと思います。

それで結局どの方法が良いか。 方法Aは無理としても、方法Bでいくか、方法Cでいくか……。 なんか面倒そうだからArduinoはやめて、もうESP-IDFだけを使えば良いのでは?(企画倒れ)

悩みますが、ここでは方法Cの「ESP-IDFにコンポーネントとしてArduinoフレームワークを組み込む」を採用します。 プロジェクトごとに設定できるのが良いですね。 試作段階でセキュリティ機能を有効にしないときは簡単にArduino IDEで開発しつつ、リリースが近づいてきたらESP-IDFでのビルドに移行といった使い方もできそうです。 また、ESP-IDFのビルドはidf.py buildなどとコマンドラインで実行できる5のでCIでも都合が良いんじゃないでしょうか。

次からはこの方法Cによる、セキュリティ機能を有効にしたArduinoのアプリケーションの開発と動作について述べていきます。

セットアップ

この方法を使うためのセットアップをしていきましょう。

まず、arduino-esp32は執筆時点で最新のv3.1.2をインストールしようと思います。 ただ、これはプロジェクトを作成する際にセットアップすることになるので、ここではスキップします。

次に、ESP-IDFが必要なのでインストールしましょう。 前回は(執筆時点で最新の)ESP-IDF v5.4を導入しましたが、残念ながらこのバージョンは使えません。 というのも、arduino-esp32がサポートするESP-IDFのバージョンは若干古く、 arduino-esp32 v3.1.2でも、ESP-IDF v5.3のサポートとなっています。

そのため、改めてESP-IDF v5.3(正確にはv5.3.2)を入れ直しましょう。 v5.4のリリースノートを見て、v5.3に戻すと何か問題があるかな?は見てみましたが、ぱっと見大丈夫そうです。

具体的なインストール手順としては、前回のv5.4をインストールしたときと同様に下記のガイドを参照すれば難しいことはありません。

Standard Toolchain Setup for Linux and macOS - ESP32-C3 - — ESP-IDF Programming Guide v5.3.2 documentation
https://docs.espressif.com/projects/esp-idf/en/v5.3.2/esp32c3/get-started/linux-macos-setup.html

特にLinux環境では、(すでにESP-IDF v5.4を導入していて共存したい場合でも)別ディレクトリにv5.3をcloneしてスクリプトを走らせてaliasの名前をget_idf_5v3などに変えるだけで済みます。 良かったですね。 共存するにあたってこの分離では不十分と感じるなら、Dockerコンテナで分離するのもありだと思います。

プロジェクトの準備

実機では基本的にUSB CDCを使うつもりです。 そのCDCを利用したexampleプロジェクトとして、hw_cdc_hello_worldがありましたのでこれをもとにして、プロジェクトを準備します。

$ idf.py create-project-from-example "espressif/arduino-esp32^3.1.2:hw_cdc_hello_world"
$ cd hw_cdc_hello_world
$ idf.py set-target esp32c3

プログラムも変えて、Lチカ、PreferencesライブラリによるNVSの読み書き6、Wi-Fi接続(NVSを使わせる)、ハードウェアUARTを使う処理も追加しました。

diff(長いので折りたたみ)
diff --git a/main/main.cpp b/main/main.cpp
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -1,6 +1,24 @@
 #include <Arduino.h>
+#include <Preferences.h>
+#include <WiFi.h>
+
+#define LED 10
+Preferences preferences;

 void setup() {
+  // LED
+  pinMode(LED, OUTPUT);
+
+  // NVS
+  preferences.begin("hidden_ns");
+
+  // 適当にWiFi.begin()
+  // NVSにアクセスさせるため
+  WiFi.begin("dummy-fake-ssid");
+
+  // Hardware UART
+  Serial0.begin(115200);
+
   // USB CDC doesn't need a baud rate
   Serial.begin();

@@ -14,5 +32,19 @@ void setup() {

 void loop() {
   Serial.println("Hello world!");
-  delay(1000);
+  Serial0.println("Hello Serial0!");
+  ESP_LOGW("main", "Hello warning!");
+
+  // NVS
+  unsigned int hidden_counter = preferences.getUInt("hidden_counter", 0);
+  Serial.printf("hidden_counter: %u\r\n", hidden_counter);
+  preferences.putUInt("hidden_counter", hidden_counter+1);
+
+  // LED & Restart
+  for (int i=30; i>=0; i--) {
+    Serial.printf("Restarting in %d seconds...\r\n", i);
+    digitalWrite(LED, (i%2==0)?HIGH:LOW);
+    delay(1000);
+  }
+  ESP.restart();
 }

このあとはidf.py menuconfigも使える、idf.py buildもできるはずです。 ちなみに、menuconfigの画面では、“Arduino Configuration” という普段は見慣れない項目も増えています。

Arduino Configuration Arduino Configuration

ここで、“Include only specific Arduino libraries”を設定すると、デフォルトでは全てビルドされるArduinoライブラリについて個別にビルドするか否かを決められます。 たとえば、(使っていない)ライブラリのビルドが失敗するときにスキップしたり、アプリケーションのサイズをなるべく小さくしたい場合に使えそうです。 また、Arduinoをコンポーネントとして使おうとするとRainMakerというコンポーネントも自動的に導入されますが、これも”Include only specific Arduino libraries”から削れます7

さて、こうしてプロジェクトの準備ができました。 一応、後述するフラッシュの読み書きで使うパーティションについても述べておくと、前回と同様にデフォルトの”Single factory app, no OTA”(下記)としています。

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  24K,
phy_init, data, phy,     0xf000,  4K,
factory,  app,  factory, 0x10000, 1M,

また、フラッシュ全体のサイズは4MBとしました。

動作確認(非セキュア編)

動作を確認していきます。 まずはこのまま、つまり何もセキュリティ機能を有効にしていない状態で動かします。

なお、ここでは実機として、独自のボード(ほぼESP32-C3-WROOM-02からピンを引き出しただけ)を使いました。

書き込み

ビルドして、USB CDC経由で書き込んでいきます。

# ビルド
$ idf.py build

# 一応フラッシュを消しておく
$ esptool.py --port /dev/ttyACM0 erase_flash
  :
Erasing flash (this may take a while)...
Chip erase completed successfully in 12.0s
Hard resetting via RTS pin...

# 書き込み
$ idf.py -p /dev/ttyACM0 flash
  :
Flash will be erased from 0x00000000 to 0x00005fff...
Flash will be erased from 0x00010000 to 0x000f1fff...
Flash will be erased from 0x00008000 to 0x00008fff...
  :
Hash of data verified.
  :

USB CDCからだと、GPIOを操作しなくてもダウンロードモードに入ってくれるので楽です。

実行

このときのコンソール出力は下記のとおり。

$ idf.py -p /dev/ttyACM0 monitor
  :
--- esp-idf-monitor 1.5.0 on /dev/ttyACM0 115200
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
  :
I (17) boot: ESP-IDF v5.3.2-584-g489d7a2b3a 2nd stage bootloader
I (17) boot: compile time Feb 15 2025 22:15:25
  :
Hello world!
W (589) main: Hello warning!
hidden_counter: 0
Restarting in 30 seconds...
Restarting in 29 seconds...
  :
Hello world!
W (562) main: Hello warning!
hidden_counter: 21
Restarting in 30 seconds...
  :

問題なく起動し、カウンタを加算、再起動という流れが見えます。

実機では、こんな感じにLEDも点灯しまた消灯する様子8も確認できました。 Lチカの様子 Lチカの様子

ちなみにget_security_infoを実行すると、当然何のセキュリティ機能も有効になっていないこともわかります。

$ esptool.py --port /dev/ttyACM0 get_security_info
  :
Security Information:
=====================
Flags: 0x00000000 (0b0)
Key Purposes: (0, 0, 0, 0, 0, 0, 12)
  BLOCK_KEY0 - USER/EMPTY
  BLOCK_KEY1 - USER/EMPTY
  BLOCK_KEY2 - USER/EMPTY
  BLOCK_KEY3 - USER/EMPTY
  BLOCK_KEY4 - USER/EMPTY
  BLOCK_KEY5 - USER/EMPTY
Chip ID: 5
API Version: 3
Secure Boot: Disabled
Flash Encryption: Disabled
SPI Boot Crypt Count (SPI_BOOT_CRYPT_CNT): 0x0
Hard resetting via RTS pin...

フラッシュ確認

このときのフラッシュも確認します。 read_flashしよう……と思いますが、Linux環境ではうまくいきませんでした。 baud rateを下げてもダメ。 この環境は仮想環境であり、うまくUSBパススルーできていないためと思いますが……。

そこで、ひとまずWindows環境にesptool.py9を入れて読み出しました。

> .\esptool.exe --port COM3 read_flash 0x0 4194304 not_secure_flash.bin
  :
Read 4194304 bytes at 0x00000000 in 364.6 seconds (92.0 kbit/s)...
Hard resetting via RTS pin...

全体をBzで開くとこんな感じに。もちろん平文。ビットマップに模様も見えますね。

フラッシュの内容(非セキュア編) フラッシュの内容(非セキュア編)

NVS領域だけ抜き出して、NVS Partition Parser Utilityで見た結果は下記のとおりです。

# 0x9000 = 36864
# 24Kibi = 24576
$ dd if=not_secure_flash.bin of=not_secure_flash_nvs.bin bs=1 skip=36864 count=24576

# -d namespaces
$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_tool/nvs_tool.py -d namespaces not_secure_flash_nvs.bin
Index : Namespace
 001  : hidden_ns
 002  : misc
 003  : nvs.net80211
 004  : phy

# -d minimal
$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_tool/nvs_tool.py -d minimal not_secure_flash_nvs.bin
Page no. 0, Status: Full
 nvs.net80211:ap.sndchan = 1
 nvs.net80211:opmode = 1
 phy:cal_data[0] = ...
 phy:cal_mac[0] = ...
 phy:cal_version = 1180
 nvs.net80211:sta.ssid[0] = ...
   :
 nvs.net80211:sta.apinfo[0] = ...

Page no. 1, Status: Active
 nvs.net80211:sta.apinfo[1] = ...
 hidden_ns:hidden_counter = 21

Page Empty

Page Empty

Page Empty

Page Empty

nvs.net80211には無線接続 (Wi-Fi) 関連の、phyには無線のキャリブレーションと思われるデータが入っています。 また、hidden_nshidden_counterには21という値が入っている様子も確認できてしまいました。

なお、phy_initパーティションの内容も見てみましたが、全部0xff(初期値)。

$ dd if=not_secure_flash.bin of=not_secure_flash_phy.bin bs=1 skip=61440 count=4096
$ hexdump -C not_secure_flash_phy.bin
00000000  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00001000

前回の記事ではphy_initについてあいまいにしてしまいましたが、ちゃんと調べたところphy_initに入るのは “PHY init data”。 でもデフォルトだとアプリケーションに埋め込まれる設定10で、ここでは設定も変えていないのでphy_initパーティションは使われません。 別にあっても無くてもどちらでも良い。 ここでは無視します。

動作確認(セキュア編)

続いてセキュリティ機能を有効にする編です。

セキュリティの有効化

前回の記事でも述べた「セキュリティの有効化」の操作を実施します。 ほとんど前回と同じなので適当に折りたたみますが、

鍵およびダイジェストの作成
# フラッシュ暗号化
$ openssl rand -out my_flash_encryption_key.bin 32

# セキュアブート
$ openssl genrsa -out secure_boot_signing_key.pem 3072
$ espsecure.py digest_sbv2_public_key --keyfile secure_boot_signing_key.pem --output digest.bin

# NVS暗号化
$ mkdir -p keys/
$ openssl rand -out keys/hmac_key.bin 32
$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py \
        generate-key --key_protect_hmac --kp_hmac_inputkey keys/hmac_key.bin --keyfile nvs_encr_key.bin
eFuseに鍵と設定を書き込む
# 鍵およびダイジェストの書き込み
$ espefuse.py --port /dev/ttyACM0 --do-not-confirm \
        burn_key BLOCK_KEY0 keys/hmac_key.bin HMAC_UP \
        burn_key BLOCK_KEY1 my_flash_encryption_key.bin XTS_AES_128_KEY \
        burn_key BLOCK_KEY2 digest.bin SECURE_BOOT_DIGEST0

# eFuseの設定
$ espefuse.py --port /dev/ttyACM0 --do-not-confirm \
        burn_efuse SPI_BOOT_CRYPT_CNT 7 \
        burn_efuse SECURE_BOOT_EN       \
        burn_efuse DIS_DOWNLOAD_ICACHE  \
        burn_efuse DIS_DIRECT_BOOT      \
        burn_efuse DIS_USB_JTAG         \
        burn_efuse DIS_PAD_JTAG         \
        burn_efuse DIS_DOWNLOAD_MANUAL_ENCRYPT \
        burn_efuse SOFT_DIS_JTAG 7

という操作を行いました。

また、idf.py menuconfigで前回同様の設定を行います。 ただし、今回はUARTのSecure Download Modeは設定しません。 もし設定してしまうとread_flashができず、フラッシュ暗号化やNVS暗号化が本当に実施されているか?確認しにくくなってしまいます。 そこで、"Security features" > "UART ROM download mode" > "UART ROM download mode (Enabled (not recommended))"を選択し、not recommended な通常の(セキュアではない)ダウンロードモードを使うことにしています。 これは後からやっぱりセキュアなダウンロードモードを使う、ということもできますので。

書き込み

セキュリティ機能の設定が済んだら、idf.py buildでビルドを実行します。 そのあとフラッシュに書き込み……とするわけですが、idf.py flashは使えません。 署名と暗号化を実施したうえで、書き込む必要があります。

具体的な手順はやっぱり前回の記事で述べたのとほとんど同じなので折りたたみますが、こうです。

書き込み手順
# ビルド
$ idf.py build

# セキュアブート用の署名
$ espsecure.py sign_data --version 2 --keyfile secure_boot_signing_key.pem --output bootloader-signed.bin build/bootloader/bootloader.bin
$ espsecure.py sign_data --version 2 --keyfile secure_boot_signing_key.pem --output hw_cdc_hello_world-signed.bin build/hw_cdc_hello_world.bin

# 暗号化
$ espsecure.py encrypt_flash_data --aes_xts --keyfile my_flash_encryption_key.bin --address 0x0 --output bootloader-enc.bin bootloader-signed.bin
$ espsecure.py encrypt_flash_data --aes_xts --keyfile my_flash_encryption_key.bin --address 0x8000 --output partition-table-enc.bin build/partition_table/partition-table.bin
$ espsecure.py encrypt_flash_data --aes_xts --keyfile my_flash_encryption_key.bin --address 0x10000 --output hw_cdc_hello_world-enc.bin hw_cdc_hello_world-signed.bin

# フラッシュ用のファイル
$ esptool.py --chip=esp32c3 merge_bin --output=flash-enc.bin --fill-flash-size=4MB --flash_mode dio --flash_freq 80m --flash_size 4MB 0x0 bootloader-enc.bin 0x10000 hw_cdc_hello_world-enc.bin 0x8000 partition-table-enc.bin

# 書き込み
$ esptool.py --no-stub --port /dev/ttyACM0 write_flash --force --flash_mode dio --flash_freq 80m --flash_size 4MB 0x0 flash-enc.bin

ここではmerge_binでフラッシュ全体のファイルを作って書き込んでいます。 別にこれは2ndステージブートローダ、パーティションテーブル、アプリケーションの領域だけを書き換えても良いはずです。(ただし初回はerase_flashしないとうまくいかないと思われる)

なお、書き込みの際や以降でesptoolを使う際は--no-stubを指定する必要があります

最初これを指定せずに書き込みを試していましたが、うまくいかない。flash_idすらこうなってうまくいかない……。

# --no-stub を指定していない場合
# "Uploading stub", "Running stub" が見える
$ esptool.py --port /dev/ttyACM0 flash_id
esptool.py v4.8.1
Serial port /dev/ttyACM0
Connecting...
Detecting chip type... ESP32-C3
Chip is ESP32-C3 (QFN32) (revision v0.4)
Features: WiFi, BLE
Crystal is 40MHz
MAC: xx:xx:xx:xx:xx:xx
Uploading stub...
Running stub...

A fatal error occurred: Invalid head of packet (0x45): Possible serial noise or corruption.

もしかしてeFuseの設定をしてセキュリティ機能が有効になってしまたっために、既存フラッシュの内容と不整合を起こして起動しなくなってしまった!? 書き込み前後でリセットしないようにしておけば良かった……。 USB CDCではなく、ハードウェアUARTを使ってダウンロードモードに入ってもうまくいかない!11 文鎮になってしまった?あーあ340円12が……。やっぱし怖いスね実機は。 と、焦り散らかしましたが--no-stubを付けたら普通にうまくいきました。

# --no-stub 指定
# "Uploading stub", "Running stub" が無い
$ esptool.py --no-stub --port /dev/ttyACM0 flash_id
esptool.py v4.8.1
Serial port /dev/ttyACM0
Connecting...
Detecting chip type... ESP32-C3
Chip is ESP32-C3 (QFN32) (revision v0.4)
Features: WiFi, BLE
Crystal is 40MHz
MAC: xx:xx:xx:xx:xx:xx
Enabling default SPI flash mode...
Manufacturer: 5e
Device: 4016
Detected flash size: 4MB
Hard resetting via RTS pin...

stubはROMブートローダの機能を拡張するものですが、セキュリティ機能が有効だと都合が悪い13みたいですね。 そういえば、前回QEMUで試したときにこんな--no-stub付けろみたいなWarningが出ていました。

WARNING: Stub loader is not supported in Secure Download Mode, setting --no-stub

Secure Download Modeではstubは使えない、なので(Secure Download Modeが有効な)前回は明示的に--no-stubを指定せずともstubの使用をスキップしてくれていたのでしょう。

実行

このときのコンソール出力は下記のとおり。

$ idf.py -p /dev/ttyACM0 monitor
  :
SPIWP:0xee
mode:DIO, clock div:1
Valid secure boot key blocks: 0
secure boot verification succeeded
  :
I (330) flash_encrypt: Flash encryption mode is RELEASE
I (336) nvs_sec_provider: NVS Encryption - Registering HMAC-based scheme...
  :
I (394) nvs: NVS partition "nvs" is encrypted.
  :
Hello world!
W (572) main: Hello warning!
hidden_counter: 62
Restarting in 30 seconds...
Restarting in 29 seconds...
  :

(1stステージブートローダによる2ndステージブートローダの)セキュアブート検証に成功していそうな、またフラッシュ暗号化とNVS暗号化が有効になっていそうなメッセージも出ていますね。 特にNVS partition "nvs" is encrypted.というメッセージは、CONFIG_NVS_ENCRYPTIONが設定されていないと出てこない14ので、静的なバイナリレベルではセキュリティ機能が効いていそうです。

ここでget_security_infoも実行すると、eFuse的にもセキュリティ機能が有効になっていることがわかります。

$ esptool.py --no-stub --port /dev/ttyACM0 get_security_info
  :
Security Information:
=====================
Flags: 0x000004f1 (0b10011110001)
Key Purposes: (8, 4, 9, 0, 0, 0, 12)
  BLOCK_KEY0 - HMAC_UP
  BLOCK_KEY1 - XTS_AES_128_KEY
  BLOCK_KEY2 - SECURE_BOOT_DIGEST0
  BLOCK_KEY3 - USER/EMPTY
  BLOCK_KEY4 - USER/EMPTY
  BLOCK_KEY5 - USER/EMPTY
Chip ID: 5
API Version: 3
Secure Boot: Enabled
Secure Boot Key Revocation Status:

        Secure Boot Key1 is Revoked

        Secure Boot Key2 is Revoked

Flash Encryption: Enabled
SPI Boot Crypt Count (SPI_BOOT_CRYPT_CNT): 0x7
Icache in UART download mode: Disabled
JTAG: Permanently Disabled
Hard resetting via RTS pin...

フラッシュ確認

フラッシュの中身も見てみましょう。read_flashしてBzで見た結果がこれです。

フラッシュの内容(セキュア編) フラッシュの内容(セキュア編)

良いですね。何も分かりません。一応、NVS領域を復号して中身見るくらいはしますか。これです。

# 抽出
$ dd if=secure_flash.bin of=secure_flash_nvs.bin bs=1 skip=36864 count=24576

# 中身は分からない
$ strings secure_flash_nvs.bin | grep hidden_counter | wc -l
0

# 復号
$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py \
        decrypt secure_flash_nvs.bin ./keys/nvs_encr_key.bin decrypt_secure_flash_nvs.bin

# 読める
$ strings decrypt_secure_flash_nvs.bin | grep hidden_counter
hidden_counter
hidden_counter
hidden_counter
hidden_counter
hidden_counter
hidden_counter
Yhidden_counter
hidden_counter
  :

# パース
$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_tool/nvs_tool.py -d minimal decrypt_secure_flash_nvs.bin
  :
Page no. 2, Status: Active
 hidden_ns:hidden_counter = 62
  :

ちゃんと意図通りの鍵で復号でき、hidden_counterの値が確認できました。

アプリ書き換え

アプリケーションを書き換えて、再起動までの時間を60秒にします。

一応diff(折りたたみ)
diff --git a/main/main.cpp b/main/main.cpp
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -41,7 +41,7 @@ void loop() {
   preferences.putUInt("hidden_counter", hidden_counter+1);

   // LED & Restart
-  for (int i=30; i>=0; i--) {
+  for (int i=60; i>=0; i--) {
     Serial.printf("Restarting in %d seconds...\r\n", i);
     digitalWrite(LED, (i%2==0)?HIGH:LOW);
     delay(1000);

普通にビルドして、アプリケーション以外に変更は無いためそこだけ署名、暗号化、書き換えを行います。

# ビルド
$ idf.py build

# 署名 & 暗号化
$ espsecure.py sign_data --version 2 --keyfile secure_boot_signing_key.pem --output hw_cdc_hello_world-60-signed.bin build/hw_cdc_hello_world.bin
$ espsecure.py encrypt_flash_data --aes_xts --keyfile my_flash_encryption_key.bin --address 0x10000 --output hw_cdc_hello_world-60-enc.bin hw_cdc_hello_world-60-signed.bin

# 書き換え
$ esptool.py --no-stub --port /dev/ttyACM0 write_flash --force --flash_mode dio --flash_freq 80m --flash_size 4MB 0x10000 hw_cdc_hello_world-60-enc.bin

そして出力を確認するとこのとおり。

$ idf.py -p /dev/ttyACM0 monitor
  :
Hello world!
W (586) main: Hello warning!
hidden_counter: 70
Restarting in 60 seconds...
Restarting in 59 seconds...
Restarting in 58 seconds...
  :

良いですね。

NVS書き換え

NVSに初期データを書いておきましょう。こんなCSVを用意します。

key,type,encoding,value
hidden_ns,namespace,,
hidden_counter,data,u32,55555

ここで書くのはhidden_nsネームスペースの、hidden_counterキーのみ。 nvs.net80211phyといったネームスペースには触れません(0xffで埋まるので、最初は存在しない扱いとなる)。

このCSVファイルをもとに、NVSのデータを作って書き込み。

$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py \
        encrypt --inputkey ./keys/nvs_encr_key.bin nvs.csv nvs.bin 0x6000
$ esptool.py --no-stub --port /dev/ttyACM0 write_flash \
        --force --flash_mode dio --flash_freq 80m --flash_size 4MB 0x9000 nvs.bin

出力を見るとこんな感じで、NVSに意図通りデータが書かれていることが分かります。

Hello world!
W (699) main: Hello warning!
hidden_counter: 55555
Restarting in 60 seconds...
  :

ちなみにしばらく経ってから実機のNVS領域を読み出すと、各種ネームスペースとデータが作られている様子も見えました。

$ esptool.py --no-stub --port /dev/ttyACM0 read_flash 0x9000 0x6000 secure_flash_nvs_55555.bin
$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py \
        decrypt secure_flash_nvs_55555.bin ./keys/nvs_encr_key.bin decrypt_secure_flash_nvs_55555.bin

$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_tool/nvs_tool.py -d namespaces decrypt_secure_flash_nvs_55555.bin
Index : Namespace
 001  : hidden_ns
 002  : misc
 003  : nvs.net80211
 004  : phy

$ python3 ${IDF_PATH}/components/nvs_flash/nvs_partition_tool/nvs_tool.py -d minimal decrypt_secure_flash_nvs_55555.bin | grep hidden_counter -B2
Page no. 6, Status: Active
 nvs.net80211:sta.apinfo[1] = b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
 hidden_ns:hidden_counter = 55561

おまけ: QEMU編

前回、QEMU環境ではArduinoのアプリケーションは動かせませんでした。 ところが、今回(少なくともESP-IDFに組み込んだとき)は普通に動いてしまっています。

$ idf.py create-project-from-example "espressif/arduino-esp32^3.1.2:hello_world"

$ cd hello_world
$ idf.py set-target esp32c3
$ idf.py build
$ idf.py qemu monitor
I (207) spi_flash: detected chip: winbond
I (208) spi_flash: flash io: dio
I (209) sleep: Configure to isolate all GPIO pins in sleep state
I (210) sleep: Enable automatic switching of GPIO sleep configuration
I (224) main_task: Started on CPU0
I (234) main_task: Calling app_main()
I (257) main_task: Returned from app_main()
I (261) uart: queue free spaces: 20
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
  :

ただし、“hw_cdc_hello_world”などUSB CDCを使おうとすると動かなくなります。 この辺はそもそも対応しているか?は未確認、どうにかして動かそうか?は考えていません。

おわりに

前回の記事、ESP32-C3のセキュリティ機能を試す(概要・予習編)とあわせて、 ESP32-C3で利用可能なセキュリティ機能「セキュアブート」「フラッシュ暗号化」「NVS暗号化」の概要まとめから、 セキュリティ機能を有効にした状態の実機でArduinoフレームワークのアプリケーションを動かすところまで行いました。

最終的に実機で動かせて良かった。 また、ESP32-C3におけるフラッシュの構造や各種ツール自体も知れて良かったです。 製造時に役立つことはもちろん、デバイスを解析する際にも役立つことでしょう。

おわり。

Footnotes

  1. PlatformIOを利用する方法もあるといえばありますが……最新のarduino-esp32は正式にはサポートされていないので外します。

  2. そしてArduino IDEが利用しているディレクトリ、具体的にはWindows環境なら%LOCALAPPDATA%\Arduino15\packages\esp32\tools\配下などに保管されている。

  3. 当該箇所verify_secure_boot_signatureが呼ばれるか否かは、CONFIG_SECURE_BOOT_V2_ENABLEDによって決定する。

  4. https://github.com/espressif/esp-idf/blob/release/v5.4/components/nvs_flash/src/nvs_api.cpp#L135-L162

  5. コマンドラインで実行したいだけなら、Arduino IDEからMakefileを作ってくれるプロジェクトもあるようです: makeEspArduino - - — Arduino ESP32 latest documentation

  6. 名前空間とキーは15文字以内(NVSの制約)、またgetおよびputの関数とvalueの型が合っていないと正しく読み書きできないので注意。

  7. ただし、menuconfigに出てくる”ESP RainMaker Config”は消えない模様。

  8. 1mAほどしか流れていないはずだが大変眩しい。マスキングテープで覆ったがまだ眩しい。

  9. バイナリが提供されているので便利。

  10. https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32c3/api-reference/kconfig.html#config-esp-phy-init-data-in-partition

  11. USB CDCかハードウェアUARTかに関わらず、--no-stubを付けたらうまくいった。

  12. https://akizukidenshi.com/catalog/g/g117493/

  13. 具体的にどの機能、どのeFuseが原因となっているか?は確認できませんでした。

  14. https://github.com/espressif/esp-idf/blob/v5.3.2/components/nvs_flash/src/nvs_api.cpp#L156