SSL/TLS の仕組みについて備忘録
備忘録と言いつつ、ほとんど参考サイトの掲載になるが、↓のサイトの「デジタル証明書の仕組み」の絵が非常に参考になります。
これで、認証局とか CSR(証明書要求)とかの登場人物がどういう関係かがわかります。最強です。感謝。
なお、自分なりに少し情報を整理してみます。
以前やった OpenSSL による SSL/TLS 環境の作成で出てきた拡張子とそれが何なのか?
ca.key
:認証局の秘密鍵と公開鍵の対情報。オレオレ認証のときは自分で作るが、普通は認証局の中で厳密に保管されているはず。ca.crt
:認証局のルート証明書。中身は認証局の公開鍵と認証局の情報をデジタル署名されたものが含まれている。これもオレオレ認証では自分で作るが、普通は認証局が公開している。server.key
:サーバーの秘密鍵と公開鍵の対情報。server.csr
:サーバーの証明書署名要求。中身はサーバーの公開鍵とコモンネーム等の各種証明書。各種証明書を認証局にデジタル署名してもらうと*.crt
になる。server.crt
:認証局によるサーバー証明書。中身はサーバー公開鍵と認証局によりディジタル署名されたサーバーの各種証明書。これは認証局の公開鍵で復号できる。
なお、↓も非常に参考になります。
mosquitto を Visual Studio でコンパイルする
githubに公開されている mosquitto のソースファイルを、Visual Studio でコンパイルします。
前提
- 今回は Visual Studio 2010 を使用する(たぶん2017とかでも大丈夫)
- Visual Studio の C++ コンパイラがインストールされていることが前提
- CMake を使用して Visual Studio のソリューションファイルを生成する
CMake を使用してソリューションファイルを生成
ここでは、CMake のインストール方法については言及しません。
mosquitto のソースファイルの用意
mosquitto のリポジトリを github からクローンします。
CMake のビルド環境を用意
必要ファイルのダウンロード
Readmeに書いてある通り、以下をそれぞれ用意する必要があります。
OpenSSL
- Link: http://slproweb.com/products/Win32OpenSSL.html
- Install "Win32 OpenSSL
" - Required DLLs: libeay32.dll ssleay32.dll
pthreads
- Link: ftp://sourceware.org/pub/pthreads-win32
- Install "pthreads-w32-
-release.zip - Required DLLs: pthreadVC2.dll
環境変数の設定
ここで、ビルドを通すために、以下の通り環境変数を設定する必要があります。
- OpenSSL
OPENSSL_ROOT_DIR
という環境変数名で、OpenSSLの の展開フォルダを設定(例:c:\temp\OpenSSL-Win32\
)
CMake のビルドを実行
CMake を起動し、以下を指定し、 Configure ボタンを押す。
Where is the source code:
に、 mosquitto のCMakeList.txt
の存在するパスWhere to build the binaries:
に、ソリューションを出力するフォルダパス
コンパイラに何を使うか聞かれるので、今回は Visual Studio 2010 を選択しました。
Finish ボタンを押すと、自動でコンパイルが走ります。
が、たぶん、 c-ares library not found
とか言って怒られるので、画面上の WITH_SRV
のチェックボックスを外し、再度 Generate すします。
正常に終了したら、CMake のコンソール上に Generating done
と表示されます。
すると、指定したフォルダに、 Visual Studio のソリューションファイルが出力されています。
参考
Visual Studio で mosquitto をビルドする
生成されたソリューションファイル(mosquitto.sln
)を起動します。
そのままビルドしてもエラーになるので、以下のようにライブラリの設定をします。
OpenSSL の環境変数
system の Path
環境変数に、OpenSSLの の展開フォルダ\bin を設定します。(例: c:\temp\OpenSSL-Win32\bin\
)
pthreads の設定
pthreads を展開したフォルダから、以下のものをコピーしていきます。
ヘッダファイル
- pthread.h
- sched.h
- semaphore.h
上記ファイルを、C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include
へコピー
lib ファイル
- pthreadVC2.lib
- pthreadVCE2.lib
- pthreadVSE2.lib
上記ファイルを、 C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib
へコピー
dll ファイル
- pthreadGC2.dll
- pthreadGCE2.dll
- pthreadVC2.dll
- pthreadVCE2.dll *pthreadVSE2.dll
上記ファイルを、 C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin
へコピー
参考
全ては時の中に… : 【VC++】スレッド(pThread)を利用する環境を整える
ビルド実行
上記でたぶん、ビルドできるはずです。
とりあえず mosquitto_sub をコンパイルしてみましたが、警告が出まくりますが、なんとか mosquitto_sub.exe が出力されました・・・
EMQ を使用して mqtt の通信をする
EMQ とは
EMQ とは、 Erlang で実装された mqtt ブローカーです。
mqtt ブローカーとしては mosquitto が有名ですが、以下の特徴があります。
- mosquitto と比較しても、 EMQ もなかなか性能が良い(らしい)
- mosquitto ではできない、ブローカーのクラスタを実現できる
- Web ブラウザの GUI が提供されているので、クライアントの管理がしやすい
Document
英語ですが、こちらに公式ドキュメントが公開されています。
インストール
環境は、 Windows 10 64bit とします。
EMQ からダウンロードして、C直下にでも解凍します。解凍するだけで、OS へのインストール作業は不要です。
ブローカーの設定
とりあえずここでは、クラスタ設定は省略します。単一ブローカーのみの接続を想定します。
C:\emqttd\etc\emq.conf
(C直下に解凍した場合)がブローカーの設定ファイルです。
IPアドレスの設定
以下を変更します。ここでは、ブローカーのIPアドレスを 192.168.0.1
を想定します。
設定キー名 | 変更前 | 変更後 |
---|---|---|
node.name | node.name = emq@127.0.0.1 | node.name = emq@192.168.0.1 |
listener.tcp.external | listener.tcp.external = 127.0.0.1:1883 | listener.tcp.external = 192.168.100.106:1883 |
listener.tcp.internal | listener.tcp.internal = 127.0.0.1:11883 | listener.tcp.internal = 192.168.100.106 |
listener.tcp.external
(たぶうんグローバルIPアドレス) は設定不要かもしれません
SSLの設定
↓に雑ですが記載しています。
ブローカーの起動
C:\emqttd
フォルダに移動し、コマンドプロンプトで以下を実行します。
bin\emqttd console
最初の起動では、ファイアウォールの許可みたいなのが出るかもしれませんが、[OK] すればよいです。
ちょっと待つと、Erlang のウィンドウが表示されます。
ウィンドウ内で (emq@192.168.0.1)1> Load emq_mod_presence module successfully.
と表示されれば、起動成功です。
Web ブラウザで確認
上記の起動方法でブローカーが正常に起動しているならば、http://localhost:18083/へアクセスするとダッシュボードが表示されます。
終了方法
Eranlg のウィンドウを閉じれば、同時にブローカーも停止します。
クラスタについて
今回は省略しましたが、クラスタに関しては、以下が参考になります。
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言語でここまでできるのはかなり魅力的だなーと思いました。
ソースコード
一応、ソースコードを載せとく。