ESP8266 から MQTT を 安全に利用する。

# 初めに
ESP8266 で MQTT を利用する記事はたくさん有ったのですが、TLS を使用して安全に利用する方法はすぐに出てこなかったので、まとめます。
今回は、TLSを利用しますが、クライアント証明書は鍵の管理が面倒なので、利用しません。

# 環境
## MQTT ブローカー
* mosquitto 1.5.8
  * ポート 8883 で待ち受け
  * Let’s Encrypt の証明書で TLS を有効化済み
  * ユーザ/パスワード で認証

## ハードウェア
* ESP-WROOM-02
    * 共立電子で購入した 4MB 版
* Arduino で使います

# Arduino のコード解説
コード自体は最後に乗せています。

```cpp
// WiFi
#define STASSID "wlanAP"
#define STAPSK "wlanPSK"

// MQTT
#define MQTT_SERVER "mqtt.hoge.fuga"
#define MQTT_PORT 8883
#define MQTT_USER "username"
#define MQTT_PASS "password"
#define MQTT_PUB_TOPIC "topicname"
#define MQTT_SUB_TOPIC "topicname"
#define MQTT_ID_PREFIX "iotdevice-"

// NTP
#define NTP_SERVER_PRIMARY "ntp.nict.jp"
#define NTP_SERVER_SECONDARY "pool.ntp.org"
#define NTP_UTC_OFFSET 60 * 60 * 9
```

* WIFIの設定など
* MQTTの認証情報など
    * MQTT_PUB_TOPIC MQTT_SUB_TOPIC は、利用するトピック名を指定してください。
    * MQTT_ID_PREFIX は、この文字列の後ろにMACアドレスを付加した物をMQTTのIDとして利用します。
* NTPの設定など
    * 証明書の検証に必要なので、NTPで時刻を合わせます。
    * 必要に応じてUTCとの時差やNTPサーバを変更してください。


```cpp
// CA
// DST_Root_CA_X3.pem
// notBefore=Sep 30 21:12:19 2000 GMT
// notAfter=Sep 30 14:01:15 2021 GMT
static const char ca_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----

-----END CERTIFICATE-----
)EOF";
```
* Let’s Encrypt のルート証明書です。
    * Let’s Encrypt 以外の場合は適宜変えてください。
    * 記載のものは2021年までなので、更新が必要な場合があるかもしれません。


```cpp
void loop()
{
    if (!mqtt.connected() && MQTTConnect())
    {
        // 接続成功
        mqtt.subscribe(MQTT_SUB_TOPIC);
        MQTTPubStr(MQTT_ID_PREFIX + getMacAddr() + " WakeUP!");
    }
    mqtt.loop();
}
```
* 接続時と再接続時に "MQTT_ID_PREFIX MACアドレス WakeUP!" を Publish します。
* MQTTPubStr で文字列を簡単に Publish できます。

```cpp
// イベントが来た際に呼ばれる
void MQTTHandle(char *topic, byte *payload, unsigned int length)
{
}
```
* Subscribe しているトピックにメッセージがあった際に呼ばれます
* https://pubsubclient.knolleary.net/api.html#callback 等を参考にしてください。

# Arduino のコード
```cpp
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <time.h>
#include <PubSubClient.h>

// WiFi
#define STASSID "wlanAP"
#define STAPSK "wlanPSK"

// MQTT
#define MQTT_SERVER "mqtt.hoge.fuga"
#define MQTT_PORT 8883
#define MQTT_USER "username"
#define MQTT_PASS "password"
#define MQTT_PUB_TOPIC "topicname"
#define MQTT_SUB_TOPIC "topicname"
#define MQTT_ID_PREFIX "iotdevice-"

// NTP
#define NTP_SERVER_PRIMARY "ntp.nict.jp"
#define NTP_SERVER_SECONDARY "pool.ntp.org"
#define NTP_UTC_OFFSET 60 * 60 * 9

// CA
// DST_Root_CA_X3.pem
// notBefore=Sep 30 21:12:19 2000 GMT
// notAfter=Sep 30 14:01:15 2021 GMT
static const char ca_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----
)EOF";

BearSSL::WiFiClientSecure wifiClient;
BearSSL::X509List cert(ca_cert);
PubSubClient mqtt(wifiClient);

void setup()
{
    Serial.begin(115200);
    Serial.println("Booting");

    // Wi-Fi接続
    WiFi.mode(WIFI_STA);
    WiFi.begin(STASSID, STAPSK);
    while (WiFi.waitForConnectResult() != WL_CONNECTED)
    {
        Serial.println("Connection Failed! Rebooting...");
        delay(5000);
        ESP.restart();
    }
    Serial.println("Ready");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // NTPで時刻合わせ
    setTimeNTP();

    // LetsEncryptの中間証明を読み込む
    wifiClient.setTrustAnchors(&cert);

    // MQTTの設定
    mqtt.setServer(MQTT_SERVER, MQTT_PORT);
    mqtt.setCallback(MQTTHandle);
}

void loop()
{
    if (!mqtt.connected() && MQTTConnect())
    {
        // 接続成功
        mqtt.subscribe(MQTT_SUB_TOPIC);
        MQTTPubStr(MQTT_ID_PREFIX + getMacAddr() + " WakeUP!");
    }
    mqtt.loop();
}

// MQTTブローカに接続を行う
bool MQTTConnect()
{
    Serial.print("Waiting MQTT connection...");
    char buf[50];
    (MQTT_ID_PREFIX + getMacAddr()).toCharArray(buf, 50);
    if (mqtt.connect(buf, MQTT_USER, MQTT_PASS))
    {
        Serial.println("connected");
        return true;
    }
    else
    {
        Serial.print("failed, rc=");
        Serial.println(mqtt.state());
        return false;
    }
}

// イベントが来た際に呼ばれる
void MQTTHandle(char *topic, byte *payload, unsigned int length)
{
}

// 文字列を投げる
void MQTTPubStr(String msg)
{
    char buf[1024];
    msg.toCharArray(buf, 1024);
    mqtt.publish(MQTT_PUB_TOPIC, buf);
}

// NTPで時刻合わせ
void setTimeNTP()
{
    Serial.print("Waiting NTP...");
    configTime(NTP_UTC_OFFSET, 0, NTP_SERVER_PRIMARY, NTP_SERVER_SECONDARY);
    // NTPの取得が終わるまで終わるまで待つ
    // 0と比較すると起動してからのカウントアップがあるので、ループから抜けてしまう
    while (time(NULL) < 365 * 24 * 60 * 60)
    {
        Serial.print(".");
        delay(1000);
    }
    Serial.println(getTime());
}

// 現在時刻を文字列で得る
String getTime()
{
    time_t t = time(NULL);
    struct tm timeinfo;
    gmtime_r(&t, &timeinfo);
    return asctime(&timeinfo);
}

// Macアドレスを文字列で得る
String getMacAddr()
{
    byte mac[6];
    char buf[50];
    WiFi.macAddress(mac);
    sprintf(buf, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    return String(buf);
}
```