EMQ で TLS 接続する
今回はEMQ(emqttd)を使用して、mqtt の TLS 接続をする方法。
EMQのインストール方法とかは割愛。
サーバーの公開鍵/暗号鍵の生成、それらのオレオレ認証
↓が参考になった。
基本的には前記事の mosquitto の場合と同じ。
EMQ側の設定を変更
これも↑のサイトのまんまになるが、一つ気を付けないといけないのが、公開鍵の生成時に「CommonName」にブローカーの FQDN を設定する必要があること。
ここは mosquitto の場合と同じで、 CommonName とブローカーのホスト名が異なると、 TLS 接続できない。
mosquitto で OpenSSL を 用いて TLS 接続する
mosquitto をで TLS 接続する方法です。
OpenSSL を使用するのが簡単だと思うけど、ちょっと間が空くとすぐやり方を忘れてしまうので備忘録として残しておきます。
2018/03/11 追記/修正
いろいろ TLS について勉強してみると、言葉の使い方や理解が正しくない箇所が複数あったので修正しました。
そして、まだ勉強不足なので間違っている箇所がある可能性は十分あります・・・。
前提
- OS : Windows 10 64bit
- mqtt ブローカーもクライアントも mosquitto を使用する
- 秘密鍵/公開鍵やサーバー証明書の生成は、OpenSSL を使用して行う
- TLS するためのサーバー証明書は、自己認証局(オレオレ認証局)で実現する
- あらかじめ pfx 形式1のものが存在しており、それを使用して自己認証局の開局する
(pfx ファイルのパスワードは把握しているものとする)
今回は 5. が特殊な条件ですが、現案件がこういう条件下なのでこれを前提で記述します。
pfx ファイルを秘密鍵/公開鍵情報とルート証明書に分離する
pfx ファイル(ca.pfx
)を OpenSSL を使用して秘密鍵/公開鍵情報(ca.key
)と自己認証局のルート証明書(ca.crt
)に分離します。
# 秘密鍵/公開鍵の取り出し $ openssl pkcs12 -in ca.pfx -nocerts -nodes -out ca.key Enter Import Password: # ca.pfx ファイルのパスワードを入力 MAC verified OK # ca.key が出力される # 自己認証局のルート証明書の取り出し $ openssl pkcs12 -in ca.pfx -clcerts -nokeys -out ca.crt Enter Import Password: # ca.pfx ファイルのパスワードを入力 MAC verified OK # ca.crt が出力される
mosquitto で TSL 接続を行う場合は、出来上がった crt ファイルをOSに「信頼できるルート証明書」として登録する必要はないので、事実上これで自己認証局の開局は済んだことになります。(mosquitto を起動する際に、ブローカーでもクライアントでも、起動引数にパスを付加することでこのルート証明書を使用します)
参考
サーバー(ブローカー)の秘密鍵/公開鍵を生成
ブローカーの TSL 接続用の秘密鍵/公開鍵として server.key
を生成します。
# 秘密鍵/公開鍵生成 $ openssl genrsa -out server.key 2048
サーバー(ブローカー)のサーバー証明書署名要求の作成
「証明書署名要求」とは、公開鍵にコモンネーム(Common Name
)等のサーバーの情報を付加したものです。
ブローカーの証明書署名要求として server.csr
を生成します。
# 証明書署名要求の生成 $ openssl req -out server.csr -key server.key -new
いろいろ入力を促されますが、とりあえずはオレオレ認証なので Common Name (e.g. server FQDN or YOUR name) []:
以外は全て未入力(そのまま Enter )でよいです。
Common Name
には、ブローカーの FQDN (ホスト名またはIPアドレス)を入力する必要があります。
もし正しく FQDN を入力しなかったら、Error: A TLS error occurred.
と言われてブローカーと接続できません。
Common Name
(コモンネーム)について追記
そもそも、SSL/TLS 接続を行う際に、クライアント側がサーバーを認証する(なりすましでないと特定する)手法が、サーバーへの接続文字列とコモンネームを比較することらしいです。
つまり、コモンネームが一致しないならば、相手が本物と特定できない(クライアントは相手がなりすましだと思わざるをえない)ので、それくらいコモンネームは重要です。
詳細は↓のサイトが非常に参考になります。
このサイトは SSL 接続の知識が体系的にまとめられていて、非常に勉強になります。
サーバー証明書の署名と発行
上記で生成したサーバー証明書署名要求を、自己認証局の秘密鍵(ca.key
)でデジタル署名して、サーバー証明書を発行します。
# サーバー証明書の署名と発行 $ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 36500
ここでは、有効期限を100年としています。(普通はこんなことしませんが、オレオレ認証なので・・・)
とりあえずここまでで、 OpenSSL を使用した TSL 接続用の鍵生成や署名は終了です。
参考
mosquitto.conf の設定
TSL 接続で使用する各証明書ファイルのパスを mosquitto.conf
に追加します。
ファイルのどこに書いても良いそうです。
// C:\Program Files (x86)\mosquitto\mosquitto.conf listener 8883 cafile ./certs/ca.crt certfile ./certs/server.crt keyfile ./certs/server.key
ここでは、上記で生成した鍵等を、 C:\Program Files (x86)\mosquitto\certs
に入れているものとしています。
動作確認
# ブローカーの起動 $ mosquitto -v -c mosquitto.conf 1520414168: mosquitto version 1.4.12 (build date 29/05/2017 11:24:45.10) starting 1520414168: Config loaded from mosquitto.conf. 1520414168: Opening ipv6 listen socket on port 8883. 1520414168: Opening ipv4 listen socket on port 8883.
# mosquitto_sub の起動 $ mosquitto_sub -d -t test -h <BrokerのFQDN> -p 8883 --cafile ./certs/ca.crt
# mosquitto_pub の起動 $ mosquitto_pub -d -t test -m aaaaaa -h <BrokerのFQDN> -p 8883 --cafile ./certs/ca.crt
Protocol Buffers の C# 版で遊んでみる
Protocol Buffers のC#版
有名どころでは、以下の2つがあるようです。
※ 追記:前者でも .proto
ファイルを使用することができるみたいです。
今回の私が手掛ける案件では、後者のほうが適する(CのサービスとC#のサービスがやりとりする)ので、ここでは Google.Protobuf を主に取り上げます。
参考サイト
C# 版 protobuf (Google.Protobuf) の導入
githubのReadmeに書かれている手順で導入していきます。
条件
以下が条件です。
- Visual Studio 2012 意向であること
- .NET4.5 以降または .NET Core であること
Nuget
Google.Protobuf を使用するだけなら、 Google.Protobuf
を Nuget すればよいです。
しかし、それに加えて .proto
ファイルを使用してクラスファイルを生成するならば、 Google.Protobuf.Tools
も Nuget する必要があります。
Google.Protobuf
はライブラリなのだが、 Google.Protobuf.Tools
はライブラリではなく、バイナリ(ptoroc.exe
)が入っています。
ちなみに、 Nuget した際のダウンロード先は、私の場合はC:\Users\XXX\.nuget\packages\google.protobuf.tools\3.5.1\tools
でした。
ソリューションファイル内にダウンロードされているものだと思っていたので、ちょっとはまってしまいました。
Google.Protobuf でチュートリアルする
とりあえず、公式のチュートリアルを実行してみます。
公式は英語なので、備忘録として意訳したやつを残しておきます。
.proto
ファイルの用意
とりあえずチュートリアルで示されている addressbook.proto
を使用します。これはgithubに掲載されているのだが、このまま使用するとエラーになる(import "google/protobuf/timestamp.proto"
なんてねーよって言われる)ので、修正したのを↓に載せときます。
// [START declaration] syntax = "proto3"; package tutorial; // [END declaration] // [START java_declaration] option java_package = "com.example.tutorial"; option java_outer_classname = "AddressBookProtos"; // [END java_declaration] // [START csharp_declaration] option csharp_namespace = "Google.Protobuf.Examples.AddressBook"; // [END csharp_declaration] // [START messages] message Person { string name = 1; int32 id = 2; // Unique ID number for this person. string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; } // Our address book file is just one of these. message AddressBook { repeated Person people = 1; } // [END messages]
.proto
ファイルからクラスファイルを生成する
Nuget した Google.Protobuf.Tools
内に含まれる tools\windows_x64\protoc.exe
を使用して、addressbook.proto
のシリアライズ/デシリアライズ用のクラスファイルを生成しあmす。
protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto
上記を実行すると、Addressbook.cs
が $DST_DIR
内に生成されます。
(Addressbook.cs
の中身を見ると、なかなかキモくて焦るが、たぶん利用する側は中身を意識する必要はそんなになさそう)
生成されたクラスファイルと Google.Protobuf を使用して、シリアライズ/デシリアライズをやってみる
チュートリアルを参考に、以下のように動作確認用の Program.cs
を作成して実行してみます。
実行結果、正しく動作していることが確認できました。
using System; using System.IO; using System.Text; using Google.Protobuf; using Google.Protobuf.Examples.AddressBook; // protoc.exeにより自動生成されたクラスの名前空間 using static Google.Protobuf.Examples.AddressBook.Person.Types; // C# 6 の書き方で、クラス内クラスを省略形式で記述することができるようになる(protobufとは関係なし) namespace ProtobufCsharp { class Program { static void Main(string[] args) { // AddressBook.csで定義されているPresonクラスを実体化する Person person = new Person { Id = 1234, Name = "Yamada Tarou", Email = "yamada@sample.com", Phones = { new PhoneNumber { Number = "555-4321", Type = PhoneType.Home }, new PhoneNumber { Number = "222-1111", Type = PhoneType.Work } } }; // 文字列にシリアライズ var data = Serialize(person); // シリアライズした文字列を読み込んでデシリアライズする var someone = Deserialize<Person>(data); // 動作確認 Console.WriteLine($"Id:{someone.Id}, Name:{someone.Name}, Email:{someone.Email}, " + $"Phones[0](Number:{someone.Phones[0].Number}, Type:{someone.Phones[0].Type}), " + $"Phones[1](Number:{someone.Phones[1].Number}, Type:{someone.Phones[1].Type})"); Console.ReadKey(); } static byte[] Serialize<T>(T obj) where T : IMessage<T> { using (var stream = new MemoryStream()) { obj.WriteTo(stream); return stream.ToArray(); } } static T Deserialize<T>(byte[] data) where T : IMessage<T>, new() { var parser = new MessageParser<T>(() => new T()); return parser.ParseFrom(new MemoryStream(data)); } } }
解説
WriteTo(stream)
メソッドで、Stream
型にシリアライズすることができるParser.ParseFrom(stream)
メソッドで、Stream
型からデシリアライズすることができる- 実際、ネットワーク間でメッセージのやり取りをする場合は、文字列が都合がいい場合が多いので、その場合は
MemoryStream
型を byte 配列に変換すればよい
とりあえず思ったより簡単に実行できました。
ただし、Stream
型を使用するシリアライズ/デシリアライズはちょっと面倒だ。直に byte 配列にできればいいのに。
ちょっと調査します。
Visual Studio 2017 で protobuf-c を試してみる
前回の続き。
もろもろの準備がやっとできたので、 Visual Studio 2017 で protobuf-c を試してみる。
前準備
あらかじめ下記内容で amessage.proto
を用意しておく(前回生成済み)
syntax = "proto3"; message AMessage { int32 a=1; int32 b=2; }
そして、protoc-c --c_out=. amessage.proto
で、以下のファイルを生成しておく(これも前回実施済み)
- amessage.pb-c.c
- amessage.pb-c.h
ソリューションの作成
上記のファイル2つと、 protobuf-c.c
および protobuf-c.h
をソリューションフォルダ内につっこむ。
この際、便宜上、以下を変更した。
amessage.pb-c.h
7: --- #include <protobuf-c/protobuf-c.h> 7: +++ #include "protobuf-c.h" // protobuf-c.hをローカルに置いたため & <>の括弧ではアクセスできないため
protobuf-c.c
316: --- return (-(uint32_t)v) * 2 - 1; 316: +++ return ((uint32_t)(-v)) * 2 - 1; // Visual Studio コンパイル時にエラーとなったため(符号なし型にマイナスをつけることができないみたい) 381: --- return (-(uint64_t)v) * 2 - 1; 381: +++ return ((uint64_t)(-v)) * 2 - 1; // Visual Studio コンパイル時にエラーとなったため(符号なし型にマイナスをつけることができないみたい) 2413: --- return -(v >> 1) - 1; 2413: +++ return -1 * (v >> 1) - 1; // Visual Studio コンパイル時にエラーとなったため(符号なし型にマイナスをつけることができないみたい) 2457: --- return -(v >> 1) - 1; 2457: +++ return -1 * (v >> 1) - 1; // Visual Studio コンパイル時にエラーとなったため(符号なし型にマイナスをつけることができないみたい) 3147: --- tmp.length_prefix_len = pref_len; 3147: +++ tmp.length_prefix_len = (uint8_t)pref_len; // 警告が出たので一応
注意
Visual Studio の環境では、これらのファイルをコンパイル対象とするために、明示的にソリューション(というかプロジェクト)構成配下に登録する必要がある。
Simple complete example チュートリアル実行
基本的に、Examples · protobuf-c/protobuf-c Wiki · GitHub を参照して、 protobuf-c の Simple complete example チュートリアルを実行してみる。
しかし以下を変更してやってみる。
結果、以下のようなファイルを用意した。
// AMessageSerialize.h #pragma once #include <windows.h> BOOL AMessageSerialize(int argc, const char * argv[]);
// AMessageSerialize.c #pragma once #include <stdio.h> #include <stdlib.h> #include "amessage.pb-c.h" #include "AMessageSerialize.h" static void MyWriteFile(void const * buf, size_t len, const char* filename); BOOL AMessageSerialize(int argc, const char * argv[]) { AMessage msg = AMESSAGE__INIT; // AMessage void *buf; // Buffer to store serialized data unsigned len; // Length of serialized data if (argc != 2 && argc != 3) { // Allow one or two integers fprintf(stderr, "usage: amessage a [b]\n"); return FALSE; } msg.a = atoi(argv[1]); msg.b = atoi(argv[2]); len = amessage__get_packed_size(&msg); buf = malloc(len); amessage__pack(&msg, buf); fprintf(stderr, "Writing %d serialized bytes\n", len); // See the length of message MyWriteFile(buf, len, "test.txt"); // Write to stdout to allow direct command line piping free(buf); // Free the allocated serialized buffer return TRUE; } static void MyWriteFile(void const * buf, size_t len, const char* filename) { FILE* fp; fopen_s(&fp, filename, "wb"); if (fp == NULL) { fprintf(stderr, "failed to write file."); goto END; } fwrite(buf, len, 1, fp); END: fclose(fp); return; }
// AMessageDeserialize.h #pragma once #include <windows.h> BOOL AMessageDeserialize();
// AMessageDeserialize.c #pragma once #include <stdio.h> #include <stdlib.h> #include "amessage.pb-c.h" #include "AMessageDeserialize.h" #define MAX_MSG_SIZE 1024 static size_t read_buffer(unsigned max_length, uint8_t *out, const char* filename) { size_t cur_len = 0; size_t nread; FILE* fp; fopen_s(&fp, filename, "rb"); if (fp == NULL) { fprintf(stderr, "failed to read file."); goto END; } while ((nread = fread(out + cur_len, 1, max_length - cur_len, fp)) != 0) { cur_len += nread; if (cur_len == max_length) { fprintf(stderr, "max message length exceeded\n"); exit(1); } } END: fclose(fp); return cur_len; } BOOL AMessageDeserialize() { AMessage *msg; // Read packed message from standard-input. uint8_t buf[MAX_MSG_SIZE]; size_t msg_len = read_buffer(MAX_MSG_SIZE, buf, "test.txt"); // Unpack the message using protobuf-c. msg = amessage__unpack(NULL, msg_len, buf); if (msg == NULL) { fprintf(stderr, "error unpacking incoming message\n"); return FALSE; } // display the message's fields. printf("Received: a=%d b=%d \n", msg->a, msg->b); // required field // Free the unpacked message amessage__free_unpacked(msg, NULL); return TRUE; }
// main.c #pragma once #include <stdio.h> #include "AMessageSerialize.h" #include "AMessageDeserialize.h" int main(int argc, const char * argv[]) { AMessageSerialize(argc, argv); AMessageDeserialize(); system("pause"); return 0; }
実行結果
引数を 10 2
で実行してみると、以下になった。
Writing 4 serialized bytes Received: a=10 b=2 続行するには何かキーを押してください . . .
んーたぶんできてるっぽい。
とりあえず目的達成。シリアライズとデシリアライズの方法も直感的だし、C言語でここまでできるのはかなり魅力的だなーと思いました。
ソースコード
一応、ソースコードを載せとく。
protobuf-c で今度こそ遊んでみる(proto2とproto3の違い編)
あらすじ
もともと軽い気持ちで Protocol Buffers のC言語版を遊んでみようと思っていたら、意図せず Docker を巻き込んだ一大イベントに発展した。
やっとこさ準備ができたので、楽しく遊ぼうとしていた。が、しかし・・・
protocol buffers のバージョン2とバージョン3は互換性がない件について
protobuf-c の wikiを読みながらチュートリアルに取り組んでみた。
amessage.proto
という名前で以下の内容のファイルを作る。
message AMessage { required int32 a=1; optional int32 b=2; }
で、さっそく protobuf-c を実行させて AMessage
構造体のシリアライザーとデシリアライザーを作ろうと以下を実行した。
root@e834aae66aee:/work/ProtocolBuffersTest# protoc-c --c_out=. amessage.proto [libprotobuf WARNING google/protobuf/compiler/parser.cc:546] No syntax specified for the proto file: amessage.proto. Please use 'syntax = "proto2";' or 'syntax = "proto3";' to specify a syntax version. (Defaulted to proto2 syntax.)
なんか警告が出る!
直訳すると、「 proto2 と proto3 のどっちで動作させるかちゃんと明記('syntax = "proto2";' か 'syntax = "proto3";')してくれ。じゃないと proto2 で動作するよ。」って感じだと思う。
なんとなくだけど、新しいほうがいいと思うので、amessage.proto
を以下のように変更してみた。
syntax = "proto3"; message AMessage { required int32 a=1; optional int32 b=2; }
で、実行するとエラーになる・・・
root@e834aae66aee:/work/ProtocolBuffersTest# protoc-c --c_out=. amessage.proto amessage.proto:5:12: Explicit 'optional' labels are disallowed in the Proto3 syntax. To define 'optional' fields in Proto3, simply remove the 'optional' label, as fields are 'optional' by default.
調べてみると、どうやら proto2 と proto3 で protoファイルの記法がかわったとのこと。
具体的には、proto3 では required
と optional
が削除されたらしい。
なんで?って感じだけど、以下の stackoverflow に質問と回答があったので、一応載せとく。よく読んでないけど、拡張性を考慮するとそれらを削除するという流れになったそうだ。
protocol buffers バージョン3を選択する
ということで、再々度 amessage.proto
を変更する。
syntax = "proto3"; message AMessage { int32 a=1; int32 b=2; }
これで、 protoc-c --c_out=. amessage.proto
が正常に動作して、以下のファイルが出力された。
- amessage.pb-c.c
- amessage.pb-c.h
引き続き、wiki の Examples に従ってチュートリアルしてみるけど、まんま wiki の通りになるので割愛します。
Protobuf-c を Docker 上の Ubuntu 環境でインストールする
あらすじ
前回、 protobuf を Docker 上の Ubuntu 環境でインストールした。
今回は、いよいよ protobuf の C言語版である protobuf-c をインストールする。
protobuf-c をインストールするまでの手順
基本的には protobuf と同様に、以下の Readme に従う。
git hub からリポジトリをクローンする
こちらからクローン。詳細は省略します。
コンパイル/インストールする
Readme に書いてある通り、./autogen.sh && ./configure && make && make install
を実行すればコンパイル&インストールができるらしい。
ちなみに、前提として以下の記事の通り、 protobuf-c をコンパイルするための Ubuntu の環境は構築済みとする。
補足
なんと、リリース版なのにコンパイルエラーになる。(protobuf-c ver1.30)
In file included from protoc-c/c_file.cc:64:0: ./protoc-c/c_file.h:107:3: error: 'vector' does not name a type vector<string> package_parts_;
調べてみると、既知の問題らしく、 issue に解決策が載っていた。
どうやら、 vector
を std::vector
に、 pair
を std::pair
に置換すればよいとのこと。
理由は英語で解説してるっぽい人がいたけど、google先生に翻訳してもらってもよくわからん・・・
とりあえず言われた通り置換すると、一応コンパイル通りました。
動作確認
protoc-c --version
を実行し、以下が出たたらOK。
root@e834aae66aee:/work/protobuf-c# protoc-c --version protobuf-c 1.3.0 libprotoc 3.5.1
いやーしかし、リリース版がコンパイル通らないって・・・なんかこれ心配になってきたぞ・・・
次回はいよいよ、 protobuf-c を使って遊んでみる!
Protobuf を Docker 上の Ubuntu 環境でインストールする
あらすじ
前回の続き
今までの流れの概略
- protobuf-c をコンパイルしようと思ったら Windows 上ではうまくいかなかった
- →Ubuntu 環境だったらうまくいくのではと思い、 Docker (Docker ToolBox)を導入してみた
- →Ubuntu のイメージを、 protobuf-c コンパイル用にカスタマイズするために Dockerfile を用意した
- →protobuf-c をコンパイルするために、本家 Protobuf が必要なのだが、 Ubuntu 用のパッケージはバージョンが古い(protobuf ver2.6.1)。protobuf-c ver1.30 に互換性問題があり、 protobuf ver3.X 以降でないと、 protobuf-c をコンパイルできない
- →protobuf ver3.X(最新版)を自分でコンパイル/インストールする
今回の目標
ということで、今回は本家 protobuf ver3.X をインストールする
protobuf をインストールするまでの手順
基本的には以下の Readme に従う。
git hub からリポジトリをクローンする
git hub のリポジトリからクローンする。
root@e834aae66aee:/# ls bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var work root@e834aae66aee:/# cd work root@e834aae66aee:/work# git clone https://github.com/google/protobuf.git Cloning into 'protobuf'... remote: Counting objects: 50405, done. remote: Compressing objects: 100% (10/10), done. remote: Total 50405 (delta 5), reused 6 (delta 3), pack-reused 50392 Receiving objects: 100% (50405/50405), 44.17 MiB | 126.00 KiB/s, done. Resolving deltas: 100% (33940/33940), done. Checking connectivity... done. Checking out files: 100% (1952/1952), done.
補足
一応、データ永続化用の共有フォルダにクローンすると、Windows であれば tortoise git とかの GUIで git 操作でき、ubuntu と windows のいいとこどりができる。
ちなみに、こういう永続化領域を Data Volume
というらしい。
コンフィグスクリプトを生成する
クローンしたら、 ./autogen.sh
を実行しなければいけないらしい。
root@e834aae66aee:/work# cd protobuf root@e834aae66aee:/work/protobuf# ./autogen.sh
補足
Readme に You can skip this step if you are using a release package (which already contains gmock and the configure script).
とあるので、もしかしたらコンパイルするだけなら上記コンフィグスクリプトは不要かもしれない。
コンパイルとインストール
Readmeによると、以下を実行するとのこと。
$ ./configure $ make $ make check $ sudo make install $ sudo ldconfig
Readmeには、環境によっては追加でいろいろやらないとコンパイルが通らないことがあるとのことだけど、私の環境では上記でコンパイル/インストールが成功したっぽい。
補足
make と make check は激遅です。無限ループしてるんじゃないかってくらい、同じようなビルドログが出力され続けて終わりません。私の環境でそれぞれ20分以上かかりました。
インストール確認
protoc --version
が正常にできればとりあえずインストールはOKと思われる。
root@e834aae66aee:/work/protobuf# protoc --version libprotoc 3.5.1
とりあえず protobuf のインストールまでできた。
次回は protobuf-c のインストールについてまとめる。
別のインストール方法
github に ubuntu で protobuf ver3.X をインストールすることを題材として issue を見つけた。
これによると、
https://github.com/google/protobuf/releases/download/v3.4.0/protoc-3.4.0-linux-x86_64.zip
でも入手してインストールできるみたい。
一応備忘録として残しておく。