ESP32のセキュアなプロトタイピング環境を作成した(secureboot,フラッシュ暗号化編)

#はじめに#
独学しているIoTセキュリティについて、集大成の意味合いを込めて現在理想と考えるセキュアなプロトタイピング環境を構築しました。
デバイス、サーバー、クライアントアプリの要素技術の勉強を含めて備忘録として分散して記していきます。
[前回](https://qiita.com/kmitsu76/items/831380bfd83bd0e7f8c6)、このプロジェクトにrainbowtypeと名前を付けました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/98edd3f2-bf64-7871-d455-3f33498bec93.png)

構成は上記となり、今回はデバイス側の実装を検討してみます。


#SecureBoot,フラッシュ暗号化について#
[ESP32-WROOM-32Eについて、従来品との違い]( https://qiita.com/kmitsu76/items/b0178405907c3b065dbd) でまとめたように、ESP32 ECO V3以前のバージョンには脆弱性があるため、ECO V3世代のESP32-WROVER-E(8MB)を用意し、確認していく。

ECO V3対応で、上記脆弱性対策も行われているSecureBootV2について、下記を読み解いていく。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v2.html
環境：
Windows10 64bit
VSCode 1.51.1
Platformio 5.0.3
platformio.ini 内の設定
platform = espressif32@2.0.0
board = esp32dev
framework = espidf

まずは、対応するesptoolv2.9-devでRSA3072で署名用の鍵を作成する。

```
C:\Users\<ユーザー名>\.platformio\packages\framework-espidf\components\esptool_py\esptool> python espsecure.py generate_signing_key --version 2 client.key
espsecure.py v2.9-dev
RSA 3072 private key in PEM format written to client.key
```

上記ドキュメントを確認すると下記の「Remote Signing of Images」記述がある。

> 本番ビルドの場合、ビルドマシン（デフォルトのesp-idfセキュアブート構成）に署名キーを設定するのではなく、リモート署名サーバーを使用することをお勧めします。espsecure.pyコマンドラインプログラムを使用して、リモートシステムでセキュアブート用のアプリイメージとパーティションテーブルデータに署名できます。

> リモート署名を使用するには、「Sign binaries during build」オプションを無効にします。秘密署名鍵は、ビルドシステムに存在する必要はありません。

> アプリイメージとパーティションテーブルがビルドされた後、ビルドシステムはespsecure.pyを使用して署名手順を出力します。

```
espsecure.py sign_data --version 2 --keyfile PRIVATE_SIGNING_KEY BINARY_FILE
```

> 上記のコマンドは、既存のバイナリにイメージの署名を追加します。–output引数を使用して、署名されたバイナリを別のファイルに書き込むことができます。

```
espsecure.py sign_data --version 2 --keyfile PRIVATE_SIGNING_KEY --output SIGNED_BINARY_FILE BINARY_FILE
```

つまり、「Sign binaries during build」が有効になっていると、署名用の鍵を設定したesp-idfでのビルドを通じてのみ署名が行える形だが、arduinoなど別のビルド済みイメージにも署名する必要があるので、このオプションは無効にして、自分で別途コマンドラインで署名を行うことにする。

フラッシュ暗号化については、Securebootと同時使用が推奨されているので、同時に設定していく。

先にデバイス個別に暗号化キーを書き込む必要がある。

```
C:\Users\＜ユーザー名＞\.platformio\packages\framework-espidf\components\esptool_py\esptool> python espsecure.py　generate_flash_encryption_key device.key
```

デバイスに書き込み。いちいちBURNと入力を強制されるが、--do-not-confirmをつければ入力が不要となる。

```
C:\Users\＜ユーザー名＞\.platformio\packages\framework-espidf\components\esptool_py\esptool>python espefuse.py --port COM4 burn_key flash_encryption device.key
espefuse.py v2.9-dev
Connecting.....
Write in efuse block 1. The eFuse block will be read and write protected (no further changes or readback). This is an irreversible operation.
Type 'BURN' (all capitals) to continue.
BURN
Burned key data. New value: 3f 4e … ed 6e
Disabling read/write to key efuse block...	
```

上記だけではフラッシュ暗号化は有効にならず、起動させたいファームウェア（この場合Factory領域のrainbowtype bootloader）をビルドする際にmenuconfigでの設定が必要になる。

vscodeのターミナル内で下記を実行すると、esp-idfのmenuconfigが起動する。

```
C:\Users\＜ユーザー名＞\.platformio\penv\Scripts\pio.exe run -t menuconfig
```

項目の移動に十字キーが使えないので、j,kキーで移動する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/175fa9de-5bc5-0855-ac0c-4aac8ce4134c.png)

「Component config」へ入る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/1bb423cf-db2c-febf-5362-d0b67fff603b.png)

「ESP32-specific」へ入る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/437962a5-7d2d-0037-3add-4c4ecaa3a8aa.png)

「Minimum Supported ESP32 Revision」を「Rev 3」に変更。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/82e60432-f49d-7ab8-1261-f8c9fb57d2af.png)

トップまで戻り、「Security features」に入る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/253b98cf-ff4f-c9d1-9c6e-6b92467d1e80.png)

「App Signing Scheme」がRSAであることを確認。
「Enable Hardware Secure Boot in bootloader」にチェック。
「select secure boot version」がEnable Secure Boot version 2であることを確認。
「Sign binaries during build」のチェックを外す。
「Enable flash encryption on boot」にチェック。
「Enable usage mode」をReleaseに設定。Development(NOT SECURE)にすると、efuseの限度回数内で暗号を解除できてしまう。

※2020/12/9追記
上記のままだと、署名したbootloaderファームウェア(bootloader.bin)がサイズオーバーしてしまい、起動できなくなる。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v2.html#bootloader-size
上記に記載されているようにブートローダーのサイズを下げるため、ログを非表示にして容量をあけることにする。
トップに戻り、「Bootloader config」に入る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/8523fe3a-b575-b016-d5e8-4cc05fbd7e1e.png)

「Bootloader log verbosity」に入る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/d14d88e3-2d45-90d5-c444-3060023df7f1.png)

「No output」に変更。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/f5d945c9-6d76-a018-1ea5-5e4b553eb522.png)

これでplatformioでビルドを行う。

今回、署名用の鍵はビルド環境で1つ、暗号化キーはデバイス個別に作成したので、今後のことを考えてこの2つを安全に保管しておく。

#Secureboot,フラッシュ暗号化の流れ#

上記の流れで、フラッシュ暗号化はデバイス個別に暗号化キーをespefuse.pyで書き込んだが、Securebootはアップロードするファームウェアに署名しておけばそこから公開鍵のハッシュをefuseに合わせて書き込んでくれるらしい。

上記で、Securebootの署名は後で個別に行うことにしておいたが、ここで署名とアップロードを行っていく。

署名が必要になるのは、bootloader.bin、partition.bin、firmware.binの3点セットになる。
これらはplatformioでビルド後、プロジェクトフォルダ内の.pio\build\esp32dev\内に生成される。

流れは下記のようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/5772cd4f-c435-72e4-a11f-34e8638b1fb8.png)

書き込みを終わり、しばらく待って起動すると下記の通り表示が行われ、Secureboot,フラッシュ暗号化が完了する。

```
ets Jul 29 2019 12:21:46

rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:2, clock div:2
secure boot v2 enabled
secure boot verification succeeded
load:0x3fff0030 len:0x4
load:0x3fff0034 len:0x758
load:0x40078000 len:0x39dc
load:0x40080400 len:0xc94
entry 0x400805f0
I (432) cpu_start: Pro cpu up.
I (432) cpu_start: Application information:
I (432) cpu_start: Project name:     rtbootloader
I (434) cpu_start: App version:      1
I (439) cpu_start: ELF file SHA256:  4b3fa7984b53cd4d...
I (445) cpu_start: ESP-IDF:          3.40100.200827
I (450) cpu_start: Starting app cpu, entry point is 0x400817fc
I (0) cpu_start: App cpu up.
I (461) heap_init: Initializing. RAM available for dynamic allocation:
I (468) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (474) heap_init: At 3FFBA170 len 00025E90 (151 KiB): DRAM
I (480) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
I (486) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
I (493) heap_init: At 40096B30 len 000094D0 (37 KiB): IRAM
I (499) cpu_start: Pro cpu start user code
I (516) flash_encrypt: Flash encryption mode is RELEASE
I (518) spi_flash: detected chip: generic
I (518) spi_flash: flash io: dio
I (521) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
```

本当に暗号化されているだろうか。
esptoolはシリアル経由でESP32のフラッシュ内容を読み出すことができるので
読み出したものをバイナリエディタで表示した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/8126c790-dd9a-c3de-96ab-893c1cec243a.png)

左が元データで右がesptoolを使って同じ番地を読み出した結果。
暗号化できている。
espefuse.pyで暗号化キーを書き込み、menuconfigでフラッシュ暗号化を設定したファームウェアを書き込んだ次の起動で、自身のフラッシュ内容を暗号化しているような動作になるようだ。

取っておいた暗号化キーを使って復号を試してみる。

```
C:\Users\＜ユーザー名＞\.platformio\packages\framework-espidf\components\esptool_py\esptool\espsecure.py decrypt_flash_data --keyfile device.key --address 0x10000 -o decrypt.bin readtest.bin
espsecure.py v2.9-dev
Using 256-bit key
```

問題なく復号化できた。
復号した内容を見ると、署名を含んだ状態で暗号化されていることが分かった。

#OTAファームウェアの取り扱い#

rainbowtype bootloader自身のSecureboot,フラッシュ暗号化は上記の通りだが、ユーザー独自のファームウェアはどのようにアップすればいいのか。

試したところ以下のようになるようだ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/d7dd2451-a112-5a76-22e7-44797f633f84.png)


まず、ユーザー独自のファームウェアは同じ署名用の鍵で署名し、esp-idfのesp_https_ota()関数でota_0パーティションに書き込みを行う。するとesp_https_ota()関数でフラッシュに書かれる際に、暗号化キーで暗号化されているようだ。

以下、署名だけされたユーザー独自のファームウェアをesp_https_ota()関数で転送した際のログになる。

```
http client setup for https://...
I (52366) esp_https_ota: Starting OTA...
I (52366) esp_https_ota: Writing to partition subtype 16 at offset 0x100000
I (63336) esp_https_ota: Connection closed
I (63336) esp_image: segment 0: paddr=0x00100020 vaddr=0x3f400020 size=0x0b2d4 ( 45780) map
I (63356) esp_image: segment 1: paddr=0x0010b2fc vaddr=0x3ffb0000 size=0x02264 (  8804)
I (63356) esp_image: segment 2: paddr=0x0010d568 vaddr=0x40080000 size=0x00404 (  1028)
I (63356) esp_image: segment 3: paddr=0x0010d974 vaddr=0x40080404 size=0x026a4 (  9892)
I (63376) esp_image: segment 4: paddr=0x00110020 vaddr=0x400d0020 size=0x2b7ec (178156) map
I (63426) esp_image: segment 5: paddr=0x0013b814 vaddr=0x40082aa8 size=0x09e74 ( 40564)
I (63436) esp_image: segment 6: paddr=0x00145690 vaddr=0x400c0000 size=0x00064 (   100)
I (63446) esp_image: Verifying image signature...
I (63446) secure_boot_v2: Verifying with RSA-PSS...
I (63486) secure_boot_v2: Signature verified successfully!
I (63496) esp_image: segment 0: paddr=0x00100020 vaddr=0x3f400020 size=0x0b2d4 ( 45780) map
I (63506) esp_image: segment 1: paddr=0x0010b2fc vaddr=0x3ffb0000 size=0x02264 (  8804)
I (63516) esp_image: segment 2: paddr=0x0010d568 vaddr=0x40080000 size=0x00404 (  1028)
I (63516) esp_image: segment 3: paddr=0x0010d974 vaddr=0x40080404 size=0x026a4 (  9892)
I (63526) esp_image: segment 4: paddr=0x00110020 vaddr=0x400d0020 size=0x2b7ec (178156) map
I (63586) esp_image: segment 5: paddr=0x0013b814 vaddr=0x40082aa8 size=0x09e74 ( 40564)
I (63596) esp_image: segment 6: paddr=0x00145690 vaddr=0x400c0000 size=0x00064 (   100)
I (63596) esp_image: Verifying image signature...
I (63606) secure_boot_v2: Verifying with RSA-PSS...
I (63636) secure_boot_v2: Signature verified successfully!
```

署名検証も問題なく、書き込めているようだが、確かめてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/9d894f39-0a9f-50e4-a229-45f916772256.png)

送り込む前のイメージとOTA後esptoolで読み出したもの、それを鍵で復号したものを並べた。問題なく暗号化されている。

efuseの状態は下記の通り。

```
C:\Users\＜ユーザー名＞\.platformio\packages\framework-espidf\components\esptool_py\esptool\espefuse.py --port COM4 summary
espefuse.py v2.9-dev
Connecting.......
EFUSE_NAME             Description = [Meaningful Value] [Readable/Writeable] (Hex Value)
----------------------------------------------------------------------------------------
Efuse fuses:
WR_DIS                 Efuse write disable mask                          = 389 R/W (0x185)
RD_DIS                 Efuse read disablemask                            = 1 R/- (0x1)
CODING_SCHEME          Efuse variable block length scheme                = 0 R/W (0x0)
KEY_STATUS             Usage of efuse block 3 (reserved)                 = 0 R/W (0x0)

Security fuses:
FLASH_CRYPT_CNT        Flash encryption mode counter                     = 127 R/- (0x7f)
UART_DOWNLOAD_DIS      Disable UART download mode (ESP32 rev3 only)      = 0 R/- (0x0)
FLASH_CRYPT_CONFIG     Flash encryption config (key tweak bits)          = 15 R/W (0xf)
CONSOLE_DEBUG_DISABLE  Disable ROM BASIC interpreter fallback            = 1 R/W (0x1)
ABS_DONE_0             secure boot enabled for bootloader                = 0 R/W (0x0)
ABS_DONE_1             secure boot abstract 1 locked                     = 1 R/W (0x1)
JTAG_DISABLE           Disable JTAG                                      = 1 R/W (0x1)
DISABLE_DL_ENCRYPT     Disable flash encryption in UART bootloader       = 1 R/W (0x1)
DISABLE_DL_DECRYPT     Disable flash decryption in UART bootloader       = 1 R/W (0x1)
DISABLE_DL_CACHE       Disable flash cache in UART bootloader            = 1 R/W (0x1)
BLK1                   Flash encryption key
  = ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? -/-
BLK2                   Secure boot key
  = 91 83 … ee 70 b3 7f R/-
BLK3                   Variable Block 3
  = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W

Config fuses:
XPD_SDIO_FORCE         Ignore MTDI pin (GPIO12) for VDD_SDIO on reset    = 0 R/W (0x0)
XPD_SDIO_REG           If XPD_SDIO_FORCE, enable VDD_SDIO reg on reset   = 0 R/W (0x0)
XPD_SDIO_TIEH          If XPD_SDIO_FORCE & XPD_SDIO_REG, 1=3.3V 0=1.8V   = 0 R/W (0x0)
CLK8M_FREQ             8MHz clock freq override                          = 51 R/W (0x33)
SPI_PAD_CONFIG_CLK     Override SD_CLK pad (GPIO6/SPICLK)                = 0 R/W (0x0)
SPI_PAD_CONFIG_Q       Override SD_DATA_0 pad (GPIO7/SPIQ)               = 0 R/W (0x0)
SPI_PAD_CONFIG_D       Override SD_DATA_1 pad (GPIO8/SPID)               = 0 R/W (0x0)
SPI_PAD_CONFIG_HD      Override SD_DATA_2 pad (GPIO9/SPIHD)              = 0 R/W (0x0)
SPI_PAD_CONFIG_CS0     Override SD_CMD pad (GPIO11/SPICS0)               = 0 R/W (0x0)
DISABLE_SDIO_HOST      Disable SDIO host                                 = 0 R/W (0x0)

Identity fuses:
MAC                    Factory MAC Address
  = 10:52:1c:88:c7:9c (CRC 0x48 OK) R/W
CHIP_VER_REV1          Silicon Revision 1                                = 1 R/W (0x1)
CHIP_VER_REV2          Silicon Revision 2                                = 1 R/W (0x1)
CHIP_VERSION           Reserved for future chip versions                 = 2 R/W (0x2)
CHIP_PACKAGE           Chip package identifier                           = 1 R/W (0x1)

Calibration fuses:
BLK3_PART_RESERVE      BLOCK3 partially served for ADC calibration data  = 0 R/W (0x0)
ADC_VREF               Voltage reference calibration                     = 1121 R/- (0x3)

Flash voltage (VDD_SDIO) determined by GPIO12 on reset (High for 1.8V, Low/NC for 3.3V).
```

Secureboot,フラッシュ暗号化済みのESP32に対し、シリアル経由のファームウェアアップデートが行えるか試した。
この場合は、以下のような流れで更新できた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/1f0121a5-20e3-acba-457d-4c4bf32ef915.png)

署名したのち、暗号化キーで暗号化して、esptool.pyでシリアル転送すると、問題なく起動してきた。

このケースは特にユーザーが触れない、Factory領域のファームウェア更新を実機で行わなくてはならない際に役立つ。そのためには、暗号化キーの安全な保管と、使用したいときに使える状態に管理することが重要になる。