セキュアエレメント使った構築事例
スイッチサイエンス/144Lab 入江田 昇
概要
M5Stack Core2 for AWSにはATECC608というセキュアエレメントが搭載されています。
これを利用し、AWS-IoTのFleetという機能を利用してデバイス認証用にクライアント証明書を発行、デバイスにインストールして安全にデバイスがAWS-IoTに接続する方法を確立しようと思います。
Fleetによるクライアント証明書発行には2通りの方法があります。
- Fleetがデバイス用秘密鍵を生成し直接クライアント証明書を発行して鍵と証明書の両方を受け取る方法
- CSRをFleetに送ってクライアント証明書だけを生成してもらう方法
後者の場合、秘密鍵がネットワークを通らない&複製ができないのでより安全なクライアント証明書認証が実現できます。今回はこの後者の手法を解説します。
セキュアエレメント(ATECC608)について
- Microchip社製
- I2Cによる制御
- 耐タンパー性をもつ
- チップごとにユニークなシリアルナンバーを持つ
- 乱数生成、SHA2ハッシュ計算
- 内部に閉じたECC秘密鍵生成
- 署名と検証機能
- 保存領域の読み出しと書き込み
- スロットごとにプロテクトをかけたり
セキュアエレメントの不揮発領域
スロット#0にはマイクロチップ社が出荷時に入れ込んでくれている秘密鍵が入っています(今回はこれを利用しました)。
-
#0~#7: ECC鍵保存スロット(各スロット36バイト)
スロット 出荷時 変更 #0 生成済み 不可 #1 特殊用途むけ 不可 #2..7 空 可 -
#8: ユーザーデータ保存スロット(416バイト)
-
#9~#14: 公開鍵または署名保存スロット(各スロット72バイト)
#8以降のスロットに特殊な圧縮形式で証明書を1つだけ保存することができる。
大まかな手順
出荷時の手順
- セキュアエレメント(ATECC608)上で証明書要求(CSR)に生成しシリアル経由でPCに送る
- それをMQTT経由でAWS-IoTのFleetに送り、クライアント証明書を発行してもらう
- 受け取った証明書をシリアル経由でデバイスに送り、その証明書をデバイスに保存する
運用時の手順
- 別途デバイスにはAWSのルートCA証明書を保存しておく(今回はファームに埋め込み)
- AWS-IoT接続時に「ルートCA証明書」と「セキュアエレメントの秘密鍵」と「保存しておいたクライアント証明書」を使ってMQTT接続する
苦渋のSDK2本立て
https://github.com/m5stack/Core2-for-AWS-IoT-EduKit が本来使いたかった本家SDKで、
これだけで完結したかったんですが、CSR(証明書要求)を構築する支援ライブラリがまだなくて、atcacert_create_csr_pemという用意された関数を呼べば作れるのはわかるんですがそのCSRの雛形作りが結構ややこしい。
本来ASN1エンコードをパースしたり生成したりが別途必要でしたが、その機能が提供されておらず、opensslで生成した雛形バイナリから書くパラメータオフセットを用意して読み込ませる仕掛けでした。
これだと実行時にCSRを構築するのが非常に難しいため、別の方法を模索したところ、
ArduinoECCX08にはそのCSR雛形を構築する機能があったので今回はこちらを採用しました。
なので
- デバイスプロビジョニング時は「Arduinoで実装したファームウェア」と「Pythonによるスクリプト」で処理しておき、
- その後「Core2-for-AWS-IoT-EduKitで実装したファームウェア」で運用することにしました。
AWS-IoT Fleet Provisionig
デバイスのプロビジョニングにはAWS-IoTのFleet(MQTTベースのプロビジョニングAPIセット)を利用します。
プロビジョニングガイドはこちら。
https://docs.aws.amazon.com/iot/latest/developerguide/iot-provision.html
大元のサンプルコードはこちら。
https://github.com/aws-samples/aws-iot-fleet-provisioning
このサンプルコードを動かすのに必要な準備は
- AWS-IoTにてプロビジョニングテンプレートの作製
- AWSルートCA証明書の取得とconfig.iniへの反映
- プロビジョニング処理を行う際に使うMQTT接続用のプロビジョニング権限を持ったクライアント証明書と秘密鍵の発行とスクリプトへの設定(サンプルコードのconfig.iniで参照できる様にする)
サンプルコードのままだと「秘密鍵」と「クライアント証明書」を生成して保存するスクリプトになっています。
上記の挙動を改造して以下の様に変更します。
- デバイスで生成したCSRをFleetに送り、デバイス個体用のクライアント証明書を発行してもらう
- クライアント証明書発行時にはあらかじめ用意しておいた権限を付与する(これはプロビジョニングテンプレートにあらかじめ設定しておく)
- 証明書をデバイスに送って保存させる
- デバイスをIoTのモノとして登録します
サンプルコードでは上記の後、新しい証明書で接続テストを行いますが、改造版では秘密鍵がデバイスにしかないのでPythonスクリプトから接続試験はできませんのでコメントアウトしました。
デバイス側のプロビジョニング処理
- セキュアエレメントに保存済みの秘密鍵を利用してCSRを構築(コモンネームにデバイスシリアルをセット)
- それをシリアルポートに出力してPC側Pythonスクリプトに送る
- シリアルポート受信にて証明書を受け取りNVS領域に保存する
Arduinoライブラリのインストール
一般公開の「ArduinoECCX08」ライブラリを「M5Stack Core2 for AWS」向けに調整したソースコードが別途以下のURLに公開されています。
https://github.com/m5stack/M5Core2/tree/master/examples/core2_for_aws
にてArduinoECCX08.zipをダウンロードして
「Arduinoライブラリフォルダ」または「Arduinoプロジェクトのsrc/フォルダ」配下に展開しておきます。
#include <M5Core2.h>
#include "src/ArduinoECCX08/ArduinoECCX08.h"
#include "src/ArduinoECCX08/utility/ECCX08CSR.h"
#include "src/ArduinoECCX08/utility/ECCX08DefaultTLSConfig.h"
String readline() {
static char buff[1024];
buff[0] = 0;
char *ptr = buff;
while (1) {
char ch = Serial.read();
if (ch > 0x7e) {
delay(10);
continue;
}
Serial.write(ch);
if (ch == '\n') {
*(ptr) = 0;
break;
} else if (ch != '\r') {
*(ptr++) = ch;
}
}
return String(buff);
}
#define PKEY_SLOT 0
String serialNumber = ECCX08.serialNumber();
if (!ECCX08CSR.begin(PKEY_SLOT, false)) {
Serial.println("Error starting CSR generation!");
while (1) delay(100);
}
ECCX08CSR.setCountryName("JP");
ECCX08CSR.setCommonName(serialNumber);
String csr = ECCX08CSR.end();
// -> PCに生成したCSRを送信(空行がデータの終わり)
Serial.println(csr); Serial.println();
String certificate = "";
Serial.println("entry signed certificate and press enter key:");
// PCからシリアルポート経由で証明書を受け取る(空行がデータの終わり)
for (String in = readline(); in.c_str()[0] != 0; in = readline()) {
certificate += in + "\n";
}
// 証明書内容PEM文字列をNVSに保存
static uint32_t nvsHandle;
nvs_flash_init();
nvs_open("parameters", NVS_READWRITE, &nvsHandle);
nvs_set_blob(nvsHandle, "certificate", certificate.c_str(), certificate.length());
nvs_close(nvsHandle);
運用ファームウェア側の接続処理
サンプルコードの記述との違いは以下の2行だけ。
#0
はECC鍵スロット0を利用する指定で、デバイス証明書のPEM文字列
はNVS領域から読み出したPEM文字列です。
mqttInitParams.pDeviceCertLocation = "デバイス証明書のPEM文字列"
mqttInitParams.pDevicePrivateKeyLocation = "#0"
ハマりポイント
プロビジョニングまわり
- プロビジョニングのconfig.iniは注意深く設定しましょうAWS-IoT側のテンプレート名と合わせるのをお忘れなく
- プロビジョニング用に発行するクレーム証明書に必要な以下の権限に注意。
- "iot:Connect"
- "iot:Publish"
- "iot:Receive"
- "iot:Subscribe"
- 関連するトピックのURIは以下の3つ(############ はAWSユーザーID)
- "arn:aws:iot:ap-northeast-1:############:topic/$aws/certificates/create/*"
- "arn:aws:iot:ap-northeast-1:############:topic/$aws/certificates/create-from-csr/*"
- "arn:aws:iot:ap-northeast-1:############:topic/$aws/provisioning-templates/<プロビジョニングテンプレート名>/provision/*"
- このクレーム証明書は権限が大きめなので漏洩に注意しましょう
- プロビジョニング時のデバイス個別証明書に与える権限(プロビジョニングテンプレートの設定)はデバイスの運用に必要な最小限の権限に絞っておきましょう
デバイス側の注意点
- ArduinoSDK時のフラッシュメモリのNVSパーティションとCore2 for AWS SDKのNVSパーティションを合わせておく必要があります
- NVSは書き込み続けるといずれ書き込めなくなりますので出荷処理の前にフラッシュイレースを推奨します
まとめ
- プロビジョニングをArduinoで、実運用をM5Stack Core2 for AWS SDKにて実装しました
- 秘密鍵をセキュアエレメントに閉じたままAWS-IoTとの接続を安全に確立することができました
- この方法のメリットは秘密鍵の複製が困難で証明書の複製が該当デバイス以外で使えないこと
- この方法のデメリットは出荷時スクリプトに強力な権限を持たせるのでその漏洩に注意しなければならないところ
公開するなら資料はスライドよりもこっちの方が見やすい様な気がする。