Advent Calendar なるもの初参加です。どうぞよろしくお願いします。
1日10円から使える超格安SIMで日本のIoTに革命を起こしたソラコムさん。そんなソラコムさんから、Arduino IDEを使って簡単にネットにつながるIoTデバイスを作れちゃう、Wio LTEが発売されました。
今まで、Arduinoからネットに繋ごうとすると、Wi-fiを経由したり、BLEでスマホからネットにつなげたりすることがほとんどでしたが、特にMaker Faireみたいな会場ではWi-fiやBLEの電波は不安定だったり、SSIDの設定やペアリングを毎回しなければならなかったりで、結構大変でした。でも、LTE回線であればWi-fiに比べて断然安定しており、安心して運用できます。
それに、いままで、屋外でIoTというと、結構の確率で、ラズパイにUSBスティックモデムというパターンだったわけですが、たかがセンサーのOn Offを送るのに、そこまでやる?という違和感も個人的にありました。
そんな中、Wio LTEという「ちょうどいい」デバイスが発売され、「IoT始まったな」という気持ちに溢れた私は、発売当日にこのデバイスをポチっとしたわけです。
話が長くなるのであとは適宜ググってください。
ソラコムの提供するサービスで、すごく便利だと私が思うのは、BeamやFunnelなどを使えば、IoTデバイス側でのクラウド送信先の設定や、データの暗号化が不要という点です。
特に AWS-IoTは、TLS必須なだけでなく、クライアント証明書が必要になるので、組み込みデバイスで1から実装するにはかなりハードルが高く、ソラコムのサーバで肩代わりしてくれるのはとても便利です。
とはいえ、Wio LTEに内蔵されているCPUはTLSに対応できるくらい高速ですし、LTEモジュール自体も、TLSの処理ができたりするので、それを活用しないのは勿体無い気もします。
加えて、ソラコムのサービスがどれだけ便利なのかというのを経験するためにも、Wio LTEから直接AWS-IoTに繋ぐのはどれだけ楽なのか、大変なのか、試してみました。
実は、結構簡単にできると思っていたのですが、実際にやってみると、自分の知識不足もあり、意外と難しかったです。
今回、2種類のコードを作りました。
1つ目は、Seeedさんのライブラリを改造してLTEモジュールでのTLS接続を試みたもの、
https://github.com/kkoiwai/WioLTEforArduino
2つ目は、新里 祐教さんのライブラリを改造してmbedTLSでの接続を試みたもの
https://github.com/kkoiwai/WioLTE_TLSClient
以上となります。曲がりなりにも接続が成功したのは、後者のみです。
いずれも、超初心者で間違ったことしている可能性が高いので、詳しい方、ツッコミをお願いします。
LTEモジュールの機能を使ってみる
搭載されているEC21-JというLTEモジュールですが、さすが今風のモジュールだけあって、いろんな機能(FTP/HTTP/NTP/HTTPS/SMTP/FTPS/SMTPS/SSL...)が付いていることがわかりました。(Spec(PDF))
なので、Wio LTEのArduino IDE用ライブラリをforkして改造させて頂き、LTEモジュールのTLS(SSL)機能を使ってAWS-IoTへの接続を試みることにしました。
結果から先に申し上げると、CA証明書、クライアント証明書、クライアント秘密鍵を設定して、AWS-IoTに接続をトライするところまで行ったのですが、不明のエラーで、接続を成功させることはできませんでした。
ATコマンドマニュアルの入手
WioLTEに内蔵されているモジュール、EC21-Jのマニュアルですが、Quectelのサイトで会員登録すれば普通にダウンロードできます。
http://www.quectel.com/product/ec21.htm
以下、ATコマンドをいろいろ試すため、Wio LTE 開発で知っておいた方がいい話と、EC21-JへATコマンドを送る方法を参考にさせて頂きました。松下さんありがとうございました。
ファイルをローカルに保存する機能があるらしい
EC21-Jには無かったのですが、類似のEC20のATコマンドのマニュアル(Quectel_EC20_FILE_AT_Commands_Manual_V1.0.pdf)を調べると、AT+QFUPL
というコマンドで、SDカードや、モジュール内のFlashやRAMに、ファイルが作成できることがわかりました。見よう見まねで、下記の通り、証明書ファイルを作成してみたら、うまくいきました。
ファイルの中身は、ArduinoエディタからSetupCAを呼ぶことで指定します。
char pem_CA[] = "-----BEGIN CERTIFICATE-----\n"
"(中略)"
"-----END CERTIFICATE-----\n";
char pem_cert[] = "-----BEGIN CERTIFICATE-----\n"
"(中略)"
"-----END CERTIFICATE-----\n";
char pem_pkey[] = "-----BEGIN RSA PRIVATE KEY-----\n"
"(中略)"
"-----END RSA PRIVATE KEY-----\n";
Wio.SetupCA(pem_CA, pem_cert, pem_pkey);
#define AWS_SSLCTXID (2) // 0 to 5
#define AWS_CA_FILEPATH "RAM:CA.pem"
#define AWS_CERT_FILEPATH "RAM:cert.pem"
#define AWS_PKEY_FILEPATH "RAM:pkey.pem"
bool WioLTE::SetupCA(const char* pem_CA, const char* pem_cert, const char* pem_pkey)
{
StringBuilder str;
if (!str.WriteFormat("AT+QFUPL=\"%s\",%d", AWS_CA_FILEPATH, strlen(pem_CA))) return false;
_Module.WriteCommand(str.GetString());
if (_Module.WaitForResponse("CONNECT", 5000) == NULL) return false;
_Module.Write(pem_CA);
if (_Module.WaitForResponse(NULL, 5000, "+QFUPL:", (ModuleSerial::WaitForResponseFlag)(ModuleSerial::WFR_START_WITH | ModuleSerial::WFR_REMOVE_START_WITH)) == NULL) return RET_ERR(-1);
if (_Module.WaitForResponse("OK", 5000, NULL, (ModuleSerial::WaitForResponseFlag) ModuleSerial::WFR_GET_NULL_STRING) == NULL) return false;
str.Clear();
if (!str.WriteFormat("AT+QFUPL=\"%s\",%d", AWS_CERT_FILEPATH, strlen(pem_cert))) return false;
_Module.WriteCommand(str.GetString());
if (_Module.WaitForResponse("CONNECT", 5000) == NULL) return false;
_Module.Write(pem_cert);
if (_Module.WaitForResponse(NULL, 5000, "+QFUPL:", (ModuleSerial::WaitForResponseFlag)(ModuleSerial::WFR_START_WITH | ModuleSerial::WFR_REMOVE_START_WITH)) == NULL) return RET_ERR(-1);
if (_Module.WaitForResponse("OK", 5000, NULL, (ModuleSerial::WaitForResponseFlag) ModuleSerial::WFR_GET_NULL_STRING) == NULL) return false;
str.Clear();
if (!str.WriteFormat("AT+QFUPL=\"%s\",%d", AWS_PKEY_FILEPATH, strlen(pem_pkey))) return false;
_Module.WriteCommand(str.GetString());
if (_Module.WaitForResponse("CONNECT", 5000) == NULL) return false;
_Module.Write(pem_pkey);
if (_Module.WaitForResponse(NULL, 5000, "+QFUPL:", (ModuleSerial::WaitForResponseFlag)(ModuleSerial::WFR_START_WITH | ModuleSerial::WFR_REMOVE_START_WITH)) == NULL) return RET_ERR(-1);
if (_Module.WaitForResponse("OK", 5000, NULL, (ModuleSerial::WaitForResponseFlag) ModuleSerial::WFR_GET_NULL_STRING) == NULL) return false;
return true;
}
ちなみに、小ネタですが、pemファイルのフォーマットは改行コードを含みます。こんな感じで正規表現をつかってちゃちゃっと変換しちゃいましょう。
TLS(SSL)関係の機能が充実しているらしい
はたまた、EC21-JのATコマンドマニュアル(Quectel_EC2x&EG9x&EM05_SSL_AT_Commands_Manual_V1.0.pdf)を調べると、AT+QSSLCFG
というコマンドで、証明書の設定ができることがわかりました。見よう見まねで、下記の通り、証明書を設定してみました。
#define AWS_SSLCTXID (2) // 0 to 5
#define AWS_CA_FILEPATH "RAM:CA.pem"
#define AWS_CERT_FILEPATH "RAM:cert.pem"
#define AWS_PKEY_FILEPATH "RAM:pkey.pem"
int WioLTE::SocketOpen(const char* host, int port, SocketType type)
{
if (host == NULL || host[0] == '\0') return RET_ERR(-1);
if (port < 0 || 65535 < port) return RET_ERR(-1);
bool connectIdUsed[CONNECT_ID_NUM];
for (int i = 0; i < CONNECT_ID_NUM; i++) connectIdUsed[i] = false;
_Module.WriteCommand("AT+QISTATE?");
const char* response;
ArgumentParser parser;
do {
if ((response = _Module.WaitForResponse("OK", 10000, "+QISTATE: ", ModuleSerial::WFR_START_WITH)) == NULL) return RET_ERR(-1);
if (strncmp(response, "+QISTATE: ", 10) == 0) {
parser.Parse(&response[10]);
if (parser.Size() >= 1) {
int connectId = atoi(parser[0]);
if (connectId < 0 || CONNECT_ID_NUM <= connectId) return RET_ERR(-1);
connectIdUsed[connectId] = true;
}
}
} while (strcmp(response, "OK") != 0);
int connectId;
for (connectId = 0; connectId < CONNECT_ID_NUM; connectId++) {
if (!connectIdUsed[connectId]) break;
}
if (connectId >= CONNECT_ID_NUM) return RET_ERR(-1);
StringBuilder str;
if (!str.WriteFormat("AT+QSSLCFG=\"sslversion\",%d,3", AWS_SSLCTXID)) return RET_ERR(-1);
if (_Module.WriteCommandAndWaitForResponse(str.GetString(), "OK", 500) == NULL) return RET_ERR(-1);
str.Clear();
if (!str.WriteFormat("AT+QSSLCFG=\"ciphersuite\",%d,0XFFFF", AWS_SSLCTXID)) return RET_ERR(-1);
if (_Module.WriteCommandAndWaitForResponse(str.GetString(), "OK", 500) == NULL) return RET_ERR(-1);
str.Clear();
if (!str.WriteFormat("AT+QSSLCFG=\"cacert\",%d,\"%s\"", AWS_SSLCTXID, AWS_CA_FILEPATH)) return RET_ERR(-1);
if (_Module.WriteCommandAndWaitForResponse(str.GetString(), "OK", 500) == NULL) return RET_ERR(-1);
str.Clear();
if (!str.WriteFormat("AT+QSSLCFG=\"clientcert\",%d,\"%s\"", AWS_SSLCTXID, AWS_CERT_FILEPATH)) return RET_ERR(-1);
if (_Module.WriteCommandAndWaitForResponse(str.GetString(), "OK", 500) == NULL) return RET_ERR(-1);
str.Clear();
if (!str.WriteFormat("AT+QSSLCFG=\"clientkey\",%d,\"%s\"", AWS_SSLCTXID, AWS_PKEY_FILEPATH)) return RET_ERR(-1);
if (_Module.WriteCommandAndWaitForResponse(str.GetString(), "OK", 500) == NULL) return RET_ERR(-1);
str.Clear();
if (!str.WriteFormat("AT+QSSLCFG=\"seclevel\",%d,2", AWS_SSLCTXID)) return RET_ERR(-1);
if (_Module.WriteCommandAndWaitForResponse(str.GetString(), "OK", 500) == NULL) return RET_ERR(-1);
str.Clear();
if (!str.WriteFormat("AT+QSSLOPEN=1,%d,%d,\"%s\",%d", AWS_SSLCTXID, connectId, host, port)) return RET_ERR(-1);
if (_Module.WriteCommandAndWaitForResponse(str.GetString(), "OK", 150000) == NULL) return RET_ERR(-1);
str.Clear();
if (!str.WriteFormat("+QSSLOPEN: %d,0", connectId)) return RET_ERR(-1);
if (_Module.WaitForResponse(str.GetString(), 150000) == NULL) return RET_ERR(-1);
return RET_OK(connectId);
}
AWS-IoTに繋いでみる(がダメだった)
と、もろもろの設定をして、AWS-IoTに繋いでみたのですが、不明なエラーが出てしまい、その先に進むことができませんでした。
ちなみにですが、接続先が https://data.iot.ap-northeast-1.amazonaws.com:8443
になっているのは、カスタムエンドポイントにすると、文字数が多すぎて、一部のATコマンドがエラーになってしまったためです。一応、AWSとしては、商用はカスタムエンドポイントに接続するようにとのことです。
Note
The default endpoint data.iot.[region].amazonaws.com is intended for testing purposes only. For production code it is strongly recommended to use the custom endpoint for your account (retrievable via the iot describe-endpoint command) to ensure best availability and reachability of the service.
mbedTLSを移植してみる
LTEモジュール側でハードウエア的に処理ができないのであれば、ソフトウエアで処理してしまおうということで、軽量なTLSライブラリのmbedTLSを使ってみることにしました。
とはいえ、組み込みプログラムはおろか、Cのプログラムさえ大学の授業ぐらいでしかまともに書いたことないので、ググりにググったところ、似たような取り組みをされている方のホームページにたどり着きました。
TlsTcpClient に助けられましたという話
TlsTcpClient とは、Particle というIoTデバイスのため、mbedTLSを実装したものです。
こちらのページを参考に、TlsTcpClientを fork させてもらい、WioLTE_TLSClient なるものを作りました。
mbedTLS の構造
今回実装してみるまで、mbedTLSの存在は知っていましたが、私にとって、謎の技術でした。どうやって接続を実現するのか初めて知ったのですが、すごくシンプルに図にするとこんな感じみたいです。
普通のネット接続であれば、自分の書くスケッチからWioLTE用のコマンドを実行しますが、mbedTLSを挟むと、mbedTLSを初期設定する時に、コールバック関数?として、WioLTEの送信・受信コマンドを指定したうえで、自作のライブラリなり、スケッチから直接なりで、mbedTLSのコマンドを実行するイメージになります。
主な修正内容
移植のために修正した部分です。
詳細はコミットログを見て頂くのが早いかと思います。
コールバック?部分の修正
MQTT用と思われる、WioLTEClientというクラスがWio LTEライブラリ内にあったので、それをmbedTLSから呼べるように細かい部分をいろいろ修正しました。
ハードウエア依存部分の修正
コンパイルエラーの出た、乱数を生成する部分と、時間を取得する部分について、修正しました。
※良い子の皆さんは決して真似しないでください。
SHA1への対応
コンパイルが通って、早速AWS-IoTへの接続を試してみたところ、CA証明書を検証する部分でエラーが出てしまいました。
! The certificate validity has expired
! The certificate has been revoked (is on a CRL)
! The certificate Common Name (CN) does not match with the expected CN
! The certificate is not correctly signed by the trusted CA
! The CRL is not correctly signed by the trusted CA
! The CRL is expired
! Certificate was missing
! Certificate verification was skipped
! Other reason (can be used by verify callback)
! The certificate validity starts in the future
! The CRL is from the future
! Usa
Server Certificates is in-valid.
調べてみると、CA証明書がSHA1で署名されているようでした。
$ openssl x509 -text -noout -in VeriSign.pem
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
18:da:d1:9e:26:7d:e8:bb:4a:21:58:cd:cc:6b:3b:4a
Signature Algorithm: sha1WithRSAEncryption
(以下略)
TlsTcpClientは、メモリ節約のためSHA1対応を外しているようだったので、ソースを持ってきて復活させました。
接続してみる
実装が終わったので、早速AWS-IoTにHTTPSでデータ送信して見ました。
無事成功! AWSのコンソールにも、ちゃんとデータが見えています。
というわけで、紆余曲折ありましたが、なんとか、Wio LTEから直接、AWS-IoTにデータの送信を行うことができました。
今後の改善
####MQTT対応
いまのところ、HTTPSにしか対応できていないので。
pubsubclientに対応させるためにはいろいろ直す部分があるのと、クラスの継承とかvirtualとか私にはハードル高すぎる。。。
####ハードウエア乱数モジュールへの対応
さすがに、なんちゃって乱数対応はどうかと思うので。
Wio LTEのCPUであるSTM32F405 には、ハードウエア乱数生成器や、暗号計算アクセラレータが入っているので、ちゃんと調べれば活用できると思います。今回は、Arduinoでそれをやる術をすぐには見つけられませんでした。。。
まとめ
これだけ苦労するのであれば、素直にSORACOM Beamを使わせてもらった方が楽だと思いました。使い方はこちら。
SORACOM Beam を使用して AWS IoT と接続する(コンソール版) | SORACOM Developers
やっぱりソラコム便利。
最後に、宣伝というか自己紹介
基本的に新しもの好き。他にも、さくらIoTとIBM BluemixとNodeRedで会議室の利用管理をしてみたり、Paizaの簡単テストスクリプトを作ってみたり、Ethereum on Dockerで証券決済システムもどきを作って見たり、Hyperledgerにも手を出したりしていますので、興味がある方は見てください。
そして自分の人生で一番バズったのはこの記事。