40
41

More than 5 years have passed since last update.

IFTTT, Beebotte, Mosquitto, SSL/TLS を使って, なるべく安全に Google Home から Raspberry Pi や ESP8266 を操作する

Last updated at Posted at 2018-01-09

概要

Google Home や Google Assistant から LAN 内部にある Raspberry Pi や ESP8266 を なるべく安全に 操作するための設定です.

特に注意したのは, トークンやパスワードを通信する際は必ず暗号化された通信経路を使う ということです. トークンやパスワードが第三者に知られてしまうと, 自宅内のデバイスが勝手に操作されてしまうかもしれません.

IFTTT や Beebotte との連携について丁寧に解説された記事がいくつかあります(各記事へのコメントはこのページ末尾の "参考" に書きました).

これらを参考にして, さらに通信経路を暗号化することで, Google Home から Raspberry Pi や ESP8266 をなるべく安全に操作できるようにします.

検討したこと

プッシュ通知をどう実現するか (プロトコル, 外部サービス)

  1. メッセージの方向が外部サーバ(IFTTT や Beebotte)から LAN 内部(ESP8266 や Raspberry Pi)への, いわゆる「プッシュ通知」
  2. でも ESP8266 や Raspberry Pi をサーバにしてポートを外部に公開したくない
  3. MQTT サービスを使えばポートを外部に公開せずに比較的簡単に「プッシュ通知」できるらしい
  4. Beebotte という MQTT ブローカーサービスを使った作例が多い
  5. 当面 Beebotte 経由で「プッシュ通知」を受け取ることにする

セキュリティをどう確保するか

  1. IFTTT の Webhooks は複雑な認証が使えないので, トークンを使った単純な認証にならざるをえない(トークンが第三者に知られると危険)
  2. トークンは定期的に変更することにする
  3. 最低限, 認証情報(トークンやパスワード)が流れる通信経路は暗号化したい
  4. でも ESP8266 はリソースが限られているためSSL/TLSで安定動作させることが難しいらしい
  5. HTTP ならば, Raspberry Pi などに nginx をインストールして SSL/TLS でリバースプロキシとして動作させて ESP8266 の負担を軽くするのがよいらしい
  6. MQTT で同様のことを実現するには, MQTT ブリッジ接続を SSL/TLS で設定すれば, ESP8266 の負担を軽くしつつ外部との通信経路を暗号化できるはず
  7. Raspberry Pi に MQTT ブローカーソフトウェアの Mosquitto をインストールして Beebotte とのブリッジ接続を SSL/TLS で設定して, ESP8266 と Beebotte を安全かつ低負荷で接続する

システム構成

想定している構成は以下の通りです. 秘密にしておかなければならないトークンが流れる通信経路は SSL/TLS で暗号化されています. Mosquitto と ESP8266 の間は暗号化なしの MQTT ですがトークンは流しません(LAN 内をメッセージが平文で流れますが).

Google Home -- IFTTT --(Webhooks over HTTPS)-- Beebotte --(MQTT over SSL/TLS)-- Raspberry Pi の Mosquitto --(MQTT)-- ESP8266

以下, 各要素をセットアップする方法について説明します.

Beebotte の設定

MQTT ブローカーサービスの Beebotte のセットアップを行います.

アカウントを作成してチャンネルとリソースを作成して, チャンネル名, リソース名, トークンをメモしておきます. 以下それぞれ channel, resource, token_XXXXXXXXXXXXXXXX として説明します.

詳しい設定方法はこちらの記事こちらの記事を参照してください.

また, Beebotte のドキュメント Beebotte MQTT SupportToken Based Authentication に目を通しておくとよいです.

なお, トークンが第三者に知られてしまったら何もかも操作されてしまうので, トークンは定期的に変更したほうが安全です.

IFTTT の設定

Web サービス同士を連携させる Web サービス IFTTT でアカウントを作成して以下のように新規アプレットを作成します.

注意: Webhooks の URL の設定で必ず https を指定してください. http だとトークンがインターネット上を平文で流れます.

項目 操作または値
- New Applet をクリック
New Applet this をクリック
Choose a service Google Assistant
Choose trigger Say a phrase with a text ingredient
What do you want to say? ラズパイで $
Language Japanese
- Create Trigger をクリック
- that をクリック
Choose action service Webhooks
Choose action Make a web request
URL https://api.beebotte.com/v1/data/publish/channel/resource?token=token_XXXXXXXXXXXX
Method POST
Content Type application/json
Body {"data":"{{TextField}}"}
- Create action をクリック
Review and finish Finish をクリック
- Check now をクリック

IFTTT と Beebotte の連携テスト

ここまで設定すると, Google Home に "ラズパイで..." と話しかけると "..." 以下が Beebotte にテキストメッセージとして送られるはずです.

Beebotte の console を開いて Secret Key, Channel, Resource を入力して Subscribe を押してしておきます.

Google Home に "ラズパイで..." と話しかけて Beebotte console の Messages に以下のようなメッセージがでれば成功です. ts はタイムスタンプです.

{ "channel": "channel", "resource": "resource", "eid": "channel.resource", "data": "...", "ts": 1515487525775 }

同様に スマートフォンの Google Assistant からもテストするとよいかもしれません.

Python で Beebotte と MQTT over SSL/TLS で接続するスクリプト

Beebotte のドキュメントには MQTT over SSL/TLS で接続するサンプルがなかったのですが, 以下の Python スクリプトで接続できます.

あらかじめ paho-mqtt というパッケージを pip でインストールしておきます. また Beebotte の CA 証明書 mqtt.beebotte.com.pem を Beebotte からダウンロードしてローカルに保存しておきます.

mqtt_beebotte.py
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import paho.mqtt.client as mqtt

HOST = 'mqtt.beebotte.com'
PORT = 8883
CA_CERTS = 'mqtt.beebotte.com.pem'
TOKEN = 'token_XXXXXXXXXXXX'
TOPIC = 'channel/resource'

def on_connect(client, userdata, flags, respons_code):
    print('status {0}'.format(respons_code))

def on_message(client, userdata, msg):
    print(msg.topic + ' ' + str(msg.payload))
    print(msg.payload.decode("utf-8"))

if __name__ == '__main__':
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message
    client.username_pw_set('token:%s' % TOKEN)
    client.tls_set(CA_CERTS)
    client.connect(HOST, PORT)
    client.subscribe(TOPIC)
    client.loop_forever()

ターミナルから mqtt_beebotte.py を実行させておいて, Google Home に "ラズパイで..." と話しかけてターミナルに以下のようなメッセージがでれば成功です.

channel/resource b'{"data":"...","ispublic":true,"ts":1515490359831}'
{"data":"...","ispublic":true,"ts":1515490359831}

このスクリプトは MQTT の Python クライアントで TLS 接続を試す という記事を参考に Beebotte 用に修正しました.

Mosquitto の設定

MQTT ブローカーソフトウェアの Mosquitto を Raspberry Pi にインストールして設定します.

なお, ESP8266 のような非力なデバイスを使わない場合は Mosquitto の設定は不要です. 直接 MQTT over SSL/TLS で Beebotte に接続すれば OK です.

Mosquitto のインストール

Raspberry Pi のターミナルから

sudo apt-get install mosquitto mosquitto-clients

設定ファイルは /etc/mosquitto/ 以下にあります.

Beebotte の CA 証明書をダウンロード

Beebotte と SSL/TLS 接続するために, あらかじめ Beebotte から CA 証明書をダウンロードして Mosquitto の証明書用ディレクトリに置きます.

curl -O https://beebotte.com/certs/mqtt.beebotte.com.pem
sudo mv mqtt.beebotte.com.pem /etc/mosquitto/certs/

詳しくは Beebotte の MQTT による接続方法を参照してください.

Mosquitto で Beebotte と SSL/TLS でブリッジ接続する設定

Mosquitto の README によると /etc/mosquitto/conf.d/ 以下に .conf ファイルを作って個別の設定を書けばよいとあるので, /etc/mosquitto/mosquitto.conf を直接編集せずに /etc/mosquitto/conf.d/bridge-to-beebotte.conf というファイルを新規作成することにします.

channel, resource, token_XXXXXXXXXXXXXXXX は環境に合わせて書き換えてください.

/etc/mosquitto/conf.d/bridge-to-beebotte.conf
connection bridge-to-beebotte
address mqtt.beebotte.com:8883
bridge_cafile /etc/mosquitto/certs/mqtt.beebotte.com.pem
cleansession true
try_private false
bridge_attempt_unsubscribe false
notifications false
remote_username token:token_XXXXXXXXXXXXXXXX
topic channel/resource out 0
topic channel/resource in 0

パラメータについては, 主にこのページの設定を参考に Beebotte 用に編集しました. 各パラメータの説明は Mosquitto のマニュアルを参照してください.

Mosquitto 再起動

restart で再起動して status でステータスをみて問題なく起動してるか確認します.

sudo service mosquitto restart
sudo service mosquitto status

Mosquitto のブリッジ接続のテスト

以下, channel, resource, token_XXXXXXXXXXXXXXXX は環境に合わせて書き換えてください.

テストのためにターミナルを3つ使います.

1つめのターミナルで, 以下のように Raspberry Pi の MQTT を subscribe しておきます (ESP8266 から Raspberry Pi に MQTT 接続してメッセージを待つことを想定).

mosquitto_sub -h (Raspberry Pi の IP) -t "channel/resource"

2つめのターミナルで, 以下のように Beebotte の MQTT を subscribe しておきます (SSL/TLS が使えるデバイスから直接 Beebotte に MQTT over SSL/TLS 接続してメッセージを待つことを想定).

mosquitto_sub -h mqtt.beebotte.com -t "channel/resource" \
  --cafile /etc/mosquitto/certs/mqtt.beebotte.com.pem \
  -u "token:token_XXXXXXXXXXXX" -p 8883

3つめのターミナルで, 以下のような MQTT メッセージを publish します(ESP8266 から Raspberry Pi に MQTT メッセージを送ることを想定)

mosquitto_pub -h (Raspberry Pi の IP) -t "channel/resource" -m "Hello World"

1つめと2つめのターミナルに, それぞれ Hello World が表示されれば成功です. 1つめのターミナルには Hellow World が2回出力されるのですが理由がよくわかりません. どなたか教えてください :)

さらに, 3つめのターミナルで, 以下のような POST リクエストを Beebotte に送ります (IFTTT の Webhooks から Beebotte に HTTPS でメッセージを送ることを想定).

curl -d '{"data":"Hello World"}' -H "Content-Type: application/json" -X POST \
  https://api.beebotte.com/v1/data/publish/channel/resource?token=token_XXXXXXXXXXXX

1つめと2つめのターミナルに, 以下のような json が表示されれば成功です.

{"data":"Hello World","ispublic":true,"ts":1515424078032}

詳しくは Beebotte の publish に関するドキュメントを参照してください.

さらに, Google Home に "ラズパイで..." と話しかけて, 1つめと2つめのターミナルに以下のような json が表示されれば成功です.

{"data":"...","ispublic":true,"ts":1515488512391}

ここまでできれば, Google Home から Raspberry Pi まで連携ができていることになります.

ESP8266 から MQTT ブローカーに接続するスケッチ

Raspberry Pi の MQTT ブローカー(Mosquitto) に接続してメッセージを待ち, メッセージが来たら LED を点灯・消灯するための回路とスケッチです.

回路図

PIN 13 -- 330 Ω -- LED -- GND

ESP8266 から Raspberry Pi に MQTT 接続するスケッチ

Arduino IDE のメニューから Sketch - Include Library - Manage Libraries... を選択して Library Manager を起動して, PubSubClientArduinoJson をあらかじめインストールしておきます.

SSID 等を環境に合わせて書き換えてください.

なお, このコード中にはトークンが含まれていません(必要ないです)

mqtt_esp8266.ino
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define SSID "SSID"
#define PASSWORD "password"

#define MQTT_SERVER "(Raspberry Pi の IP)"
#define MQTT_PORT 1883
#define CLIENT_ID "ESP8266Client"
#define TOPIC "channel/resource"

#define LED_PIN 13

#define MAX_BUF 256

WiFiClient espClient;
PubSubClient client(espClient);

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(74880);

  WiFi.begin(SSID, PASSWORD);
  Serial.println(SSID);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.println(WiFi.localIP());

  client.setServer(MQTT_SERVER, MQTT_PORT);
  client.setCallback(callback);
  if (client.connect(CLIENT_ID)) {
    client.subscribe(TOPIC);
  }
}

void loop() {
  while (!client.connected()) {
    if (client.connect(CLIENT_ID)) {
      client.subscribe(TOPIC);
    } else {
      delay(5000);
    }
  }
  client.loop();
}

void callback(char* topic, byte* payload, unsigned int len) {
  // copy from payload to buf
  char buf[MAX_BUF];
  int end = len < MAX_BUF - 1 ? len : MAX_BUF - 1;
  for (int i = 0; i < end; i++) {
    buf[i] = (char)payload[i];
  }
  buf[end] = 0;
  Serial.println(buf);

  // convert buf to json
  StaticJsonBuffer<MAX_BUF> jsonBuffer;
  JsonObject& json = jsonBuffer.parseObject(buf);
  String data = json["data"];
  Serial.println(data);

  // commands
  if (-1 != data.indexOf("LED を つけ て")) {
    digitalWrite(LED_PIN, HIGH);
  } else if (-1 != data.indexOf("LED を 消し て")) {
    digitalWrite(LED_PIN, LOW);
  }
}

ESP8266 から Beebotte に MQTT over SSL/TLS で直接接続するスケッチ

ついでに ESP8266 から Beebotte に MQTT over SSL/TLS で直接接続するスケッチも書いておきます. 通常の MQTT に比べて負荷が高いため ESP8266 で長期間安定動作するかどうかはわかりません(要検証).

Beebotte に直接接続するのでトークンを送る必要がありますが, トークンを送る前に通信相手が本当に mqtt.beebotte.com であるかどうかを検証(verify)する必要があります.

通常のコンピュータではサーバの CA 証明書を用いて検証を行いますが, リソースの限られている ESP8266 では CA 証明書の代わりに CA 証明書の SHA1 fingerprint で接続先の確認をするということらしいです.

まず, ターミナルから以下のコマンドを実行して CA 証明書の fingerprint を取得しておきます.

openssl x509 -fingerprint -in mqtt.beebotte.com.pem

出力の 1 行目に以下のような文字列があるので, コロンを空白に置換してソース中の FINGERPRINT に書いておきます.

SHA1 Fingerprint=XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX

スケッチはこちらです. fingerprint の検証に失敗した場合は exit(1) します. なお証明書には期限があるので, もし検証に失敗したら最新の CA 証明書を取得し直して fingerprint を計算し直してみてください.

SSID 等を環境に合わせて書き換えてください.

mqtt_tls_esp8266.ino
// https://github.com/esp8266/Arduino/blob/master/doc/esp8266wifi/client-secure-examples.rst
// https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/examples/HTTPSRequest/HTTPSRequest.ino
// https://github.com/knolleary/pubsubclient/issues/84#issuecomment-173677183
// https://gist.github.com/chaeplin/3223074601733fa46d4a

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define SSID "SSID"
#define PASSWORD "password"

#define MQTT_SERVER "mqtt.beebotte.com"
#define MQTT_PORT 8883
#define CLIENT_ID "ESP8266Client"
#define TOPIC "channel/resource"
#define TOKEN "token:token_XXXXXXXXXXXX
// openssl x509 -fingerprint -in mqtt.beebotte.com.pem
#define FINGERPRINT "XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX"

#define LED_PIN 13

#define MAX_BUF 256

WiFiClientSecure espClient;
PubSubClient client(espClient);

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(74880);

  WiFi.begin(SSID, PASSWORD);
  Serial.println(SSID);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.println(WiFi.localIP());

  // need to verify fingerprint BEFORE sending token
  espClient.connect(MQTT_SERVER, MQTT_PORT);
  if (espClient.verify(FINGERPRINT, MQTT_SERVER)) {
    Serial.println("fingerprint matches");
  } else {
    Serial.println("fingerprint doesn't match");
    exit(1);
  }
  espClient.stop();

  client.setServer(MQTT_SERVER, MQTT_PORT);
  client.setCallback(callback);
  if (client.connect(CLIENT_ID, TOKEN, "")) {
    client.subscribe(TOPIC);
  }
}

void loop() {
  while (!client.connected()) {
    if (client.connect(CLIENT_ID, TOKEN, "")) {
      client.subscribe(TOPIC);
    } else {
      delay(5000);
    }
  }
  client.loop();
}

void callback(char* topic, byte* payload, unsigned int len) {
  // copy from payload to buf
  char buf[MAX_BUF];
  int end = len < MAX_BUF - 1 ? len : MAX_BUF - 1;
  for (int i = 0; i < end; i++) {
    buf[i] = (char)payload[i];
  }
  buf[end] = 0;
  Serial.println(buf);

  // convert buf to json
  StaticJsonBuffer<MAX_BUF> jsonBuffer;
  JsonObject& json = jsonBuffer.parseObject(buf);
  String data = json["data"];
  Serial.println(data);

  // commands
  if (-1 != data.indexOf("LED を つけ て")) {
    digitalWrite(LED_PIN, HIGH);
  } else if (-1 != data.indexOf("LED を 消し て")) {
    digitalWrite(LED_PIN, LOW);
  }
}

テスト

Google Home から "ラズパイでLEDをつけて" と話しかけて ESP8266 の LED が点灯したら成功です. 同様に "ラズパイでLEDを消して" で消灯するはずです.

同様に スマートフォンの Google Assistant からもテストするとよいかもしれません.

"ラズパイで..." と話しかけて ESP8266 を操作するのはちょっと変な感じがするので IFTTT のアプレットを修正して別のメッセージにするとよいかもしれません.

以上で, Google Home から ESP8266 まで連携ができたことになります.

まとめ

IFTTT, Beebotte, Mosquitto, SSL/TLS を使って, なるべく安全に Google Home から Raspberry Pi や ESP8266 を操作することができました. ポイントは以下です.

  1. 秘密にしなければならないトークンを通信する際は必ず暗号化された通信経路を使う
  2. 低リソースなデバイスも操作可能 (HTTP より軽い MQTTを使う. 重い SSL/TLS は Raspberry Pi に任せる)
  3. トークンを定期的に変更するとより安全

参考

以上

40
41
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
41