はじめに#
独学しているIoTセキュリティについて、集大成の意味合いを込めて現在理想と考えるセキュアなプロトタイピング環境を構築しました。
デバイス、サーバー、クライアントアプリの要素技術の勉強を含めて備忘録として分散して記していきます。
前回、このプロジェクトにrainbowtypeと名前を付けました。
構成は上記となり、今回はデバイス側の実装を検討してみます。
ブートシーケンスの確認#
上記のrainbowtype bootloaderの構成を検討していく。
ESP32の起動シーケンスは、搭載されたフラッシュ領域に置かれるCSVファイルのパーティションテーブルを参照し、動作を決定していく。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/general-notes.html#first-stage-bootloader
上記によると、ROMのFirst-stage bootloaderは、フラッシュ上の2nd stage bootloaderを呼び出し、その中でPartition tableを参照し、起動すべきパーティションを決定している。
図にすると下記のような形。
ESP-IDFで定義されるパーティションテーブルは下記の通りである。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/partition-tables.html
デフォルトのパーティションテーブルが2種類あり、これを読み解いていく。
「単一ファクトリアプリ、OTAなし」構成
#ESP-IDFパーティションテーブル
#名前、タイプ、サブタイプ、オフセット、サイズ、フラグ
nvs、 data、nvs、 0x9000、0x6000、
phy_init、data、phy、 0xf000、0x1000、
factory、 app、 factory、 0x10000、1M、
フラッシュの0x10000(64KB)オフセットには、「factory」というラベルの付いたアプリがあります。ブートローダーはデフォルトでこのアプリを実行します。
NVSライブラリパーティションとPHY初期化データを格納するためにパーティションテーブルに定義された2つのデータ領域もあります。
「ファクトリアプリ、2つのOTA定義」構成
#ESP-IDFパーティションテーブル
#名前、タイプ、サブタイプ、オフセット、サイズ、フラグ
nvs、 data、nvs、 0x9000、0x4000、
otadata、 data、ota、 0xd000、0x2000、
phy_init、data、phy、 0xf000、0x1000、
factory、 app、 factory、 0x10000、1M、
ota_0、 app、 ota_0、 0x110000、1M、
ota_1、 app、 ota_1、 0x210000、1M、
現在、3つのアプリパーティション定義があります。ファクトリアプリのタイプ(0x10000)と次の2つの「OTA」アプリはすべて「app」に設定されていますが、それらのサブタイプは異なります。
OTA更新用のデータを保持する新しい「otadata」スロットもあります。ブートローダーは、実行するアプリを知るためにこのデータを参照します。 「otadata」が空の場合、Factoryアプリを実行します。
上記から、
・Factory、ota_0、ota_1というパーティション領域がある。
・OTA更新用のデータを保持する「otadata」というパーティションがあり、どのOTAパーティションから起動するかデータを保持している。その内容が空の場合、Factoryパーティションが実行される。
ということが分かった。
Factory領域というのはどうもメーカー側が使用する、サービスモードのようなアプリの組み込みを行える領域のようである。
Factory領域のコントロール#
本来なら、rainbowtype bootloaderは機能上ブートシーケンスに割り込むべきだが、2nd stage bootloaderに手を入れることになり大変になること、また、ユーザーのアプリケーションの起動が大幅に遅くなるため、このFactory領域を利用し必要な時にrainbowtype bootloaderを起動させるよう構成を考えてみたい。
その場合、Factory領域をコントロールできる必要がある。
少なくとも、
Factory領域への入り方
Factory領域からapp領域へのブート移行
については抑える必要がある。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/bootloader.html#factory-reset
上記を読み解きながら試行した結果、下記のように制御できた。
環境:
Windows10 64bit
VSCode 1.51.1
Platformio 5.0.3
platformio.ini 内の設定
platform = espressif32@2.0.0
board = esp32dev
framework = espidf
vscodeのターミナル内で下記を実行すると、esp-idfのmenuconfigが起動する。
C:\Users\<ユーザー名>\.platformio\penv\Scripts\pio.exe run -t menuconfig
項目の移動に十字キーが使えないので、j,kキーで移動する。
「Bootloader Config」に入る。
ここで、「GPIO triggers factory reset」を選択すると、起動時にGPIOの状態を見てFactory領域を起動する設定が有効になる。
「Number of the GPIO input for factory reset」はどのピンを上記操作ように割り当てるか、
「Hold time of GPIO for reset/test mode (seconds)」は何秒GPIOをGNDに接続したらFactory領域を起動させるかを設定する。
今回はGPIO15を2秒GNDに接続する設定とした。
また、「Clear OTA data on factory reset (select factory parttion)」を設定すると、Factory領域がいったん起動したら、otadataパーティションを初期化することで、以後Factory領域がGPIOを押さえなくても起動するようになる。
上記をsdkconfigの設定に反映させると下記のようになった。
CONFIG_BOOTLOADER_FACTORY_RESET=y
CONFIG_BOOTLOADER_HOLD_TIME_GPIO=2
CONFIG_BOOTLOADER_OTA_DATA_ERASE=y
CONFIG_BOOTLOADER_NUM_PIN_FACTORY_RESET=15
Factory領域の起動について制御できたが、そのあとは、Factory領域からApp領域へOTAでファームウェアを転送するか、
esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL));
上記関数を実行してやると、初期化されたotadataパーティションが設定され、リセットすると再びApp領域が起動するようになる。
ここまでできたところで、rainbowtype bootloaderのパーティションを下記の通りとした。
nvs, data, nvs, 0xa000, 0x3000
otadata, data, ota, 0xd000, 0x2000
phy_init,data, phy, 0xf000, 0x1000
factory, app, factory,0x10000,0xF0000
ota_0, app, ota_0, ,0x700000
8MBフラッシュのESP32用で、OTA領域は2面でなく大きな1面領域で、大きなプログラムも置けるように考慮した。
参考までにArduino-ESP32でデフォルトのパーティションテーブルは下記の通りになる。
https://github.com/espressif/arduino-esp32/blob/master/tools/partitions/minimal.csv
# Name Type SubType Offset Size Flags
nvs data nvs 0x9000 0x5000
otadata data ota 0xe000 0x2000
app0 app ota_0 0x10000 0x140000
spiffs data spiffs 0x150000 0xB0000
Factory領域は使っておらず、app0が直接起動する設定となる。