non-blocking socketにOpenSSL適用する


この記事で使用するOpenSSLのバージョンは1.0.1uです。
ハートブリード脆弱性を回避するには、1.0.1g以上を使用します。

ここではOpenSSLのビルド、プロジェクトに設定する方法、詳細なエラー処理は省略します。
開発に必要な内容だけ整理してみよう。

以下の内容があればすぐにnon-blocking socketにSSLを適用できます。
この記事で必要なヘッダファイルは以下のとおりです。

ssh.h
bio.h
err.h
engine.h
conf.h

プライベート証明書の生成

テストのために証明書が必要なので、プライベート証明書を1つ作ろう。
以下のように実行すればプライベート証明書を作成できます。

openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem

OpenSSLの初期化/解除

OpenSSLを使用するために、以下のように初期化を行います。

SSL_library_init();
SSL_load_error_strings();
ERR_load_BIO_strings();

SSLを使用するcontextオブジェクトが必要で、以下のように使用するSSLバージョンを選択して、SSL Contextを生成します。
ここではTLSバージョン1.2を指定しました。

SSLCtx = SSL_CTX_new(TLSv1_2_server_method());

先ほど作成したプライベート証明書をロードするように、以下のように設定して、SSL_CTX_check_private_keyで検証します。

SSL_CTX_use_certificate_file(SSLCtx, "mycert.pem", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(SSLCtx, "mycert.pem", SSL_FILETYPE_PEM);
SSL_CTX_check_private_key(SSLCtx);

SSL_CTX_check_private_keyがエラーを返す場合、証明書に問題があります。
SSLの使用が終了したらSSL_CTX_freeで下記のように使用したすべてのリソースを解除します。

SSL_CTX_free(SSLCtx);

ERR_remove_state(0);
ENGINE_cleanup();
CONF_modules_unload(1);
ERR_free_strings();
EVP_cleanup();
sk_SSL_COMP_free(SSL_COMP_get_compression_methods());
CRYPTO_cleanup_all_ex_data();

SSL Accept処理

SSL Accept処理はTCP Acceptが処理された後に進行します。
SSL接続管理に必要なSSL構造体をSSL_new()から生成し、SSLハンドシェイク処理をSSL_accept()に委任するため、TCP socket handleに接続する必要があります。
接続するTCP socket handleは、TCP acceptが完了したsocketのhandleであるべきで、SSL_accept()の呼び出し前にsocketをblocking modeに設定し、SSL_accept()が完了してから再度non-blocking modeに変えるのが簡単でしょう。

SSL* ssl = SSL_new(SSLCtx);    // SSLCtxは上からSSL_CTX_new()に生成したものである
SSL_set_fd(ssl, static_cast<int>(socketHandle));
SSL_accept(ssl);

SSL non-Blocking通信

SSL Accept処理が完了した後、簡単にSSL_read()、SSL_write()を使って暗号化通信を使用できますが、基本的にblocking modeで動作します。
Memory BIOを使ってSSL処理をTCP socket通信処理と分離すると、簡単に既存のnon-blocking socketにSSLを適用できます。
Memory BIOを使用するには、以下のようにread、writeのBIOを作成する必要があり、上からSSL_new()で生成されたSSLと接続する必要があります。

BIO* rbio = BIO_new(BIO_s_mem());
BIO* wbio = BIO_new(BIO_s_mem());
SSL_set_bio(ssl, rbio, wbio);

SSL_accept()処理で接続していたsocket handleと分離したことで、socketとの依存関係がなくなりました。
既存のsocket通信ロジックはそのまま使用し、send/recv処理の前に暗号化処理のみを追加すればよいでしょう。暗号化処理は以下のとおりです。

// sslは上からSSL_newに生成したものである
// SSL_writeは、sourceメモリの内容を暗号化して連結したBIOメモリに記録する
int writtenLength = SSL_write(ssl, sourceMemoryPointer, lengthForWrite);

int bioWrittenLength = BIO_number_written(wbio);    // 暗号化された内容の長さをもたらす

// 暗号化された内容をtargetメモリにコピーする
int len = BIO_read(wbio, targetMemoryPointer, bioWrittenLength);

暗号化処理は、任意の内容をネットワークにエクスポートする前に、一度に暗号化して処理する方が便利です。復号化処理はこれとは若干異なりますが、送信側が1000byteを送信しても、受信側が1000byteを一度に受信できる保証がないからです。復号化処理は以下のとおりです。

// sslは上からSSL_newに生成したものである
// BIO_writeはsourceメモリの内容を復号し、連結したSSLで読めるようにする
int writtenLength = BIO_write(rbio, sourceMemoryPointer, lengthForWrite);

int bioWrittenLength = BIO_number_written(wbio); // 復号された内容の長さをもたらす

/* 
復号された内容をtargetメモリにコピーする
SSL_read()がリターンした値が0より小さければ、SSL_get_error(ssl, len);を呼び出し、error codeがSSL_ERROR_WANT_READかどうか確認する。SSL_ERROR_WANT_READであれば、暗号化された内容がすべて届いておらず、復号化に失敗している。
*/
int len = SSL_read(ssl, targetMemoryPointer, bioWrittenLength);

SSL_ERROR_WANT_READの処理は、追加の受信パケットを受けてBIO_write()に書き続ければよいでしょう。
簡単に言うと、SSL_ERROR_WANT_READが発生した場合、上記の処理をもう一度やり直せばよいのです。
暗号化された内容がすべて揃わないと復号化できないために発生するもので、当然の処理であり例外事項ではありません。

SSL Close処理

socket close処理をするときにsocketのために作成したSSLで、まとめて整理する必要があります。
Memory BIOを使ったので、SSL Accept処理後はsocket handleとの依存関係が消え、socket handleのclose順序とは関係がありません。
以下のように整理すればよいでしょう。

SSL_shutdown(ssl);
SSL_free(ssl);    // このとき連結されたBIOもすべて解除される

簡単なSSL証明書と送受信テスト

OpenSSLをインストールしたとき一緒に設置されるopenssl command line toolを使って簡単なテストを行うことができます。
上記の内容を実装したSSL serverを実行して、以下のようにopenssl command line toolを実行します。このときSSL portは443と仮定します。

openssl s_client -connect 127.0.0.1:443 -debug -tls1_2

上記で使用した引数は、SSL clientモードでローカルホストのポート443で、TLS 1.2プロトコルを使ってdebugモードで接続する、というものです。
debugモードでは、追加情報が表示されます。
実行して接続に成功すると、SSLハンドシェイク過程と、証明書の情報が出力され、ユーザーの入力を待ちます。
このとき簡単なチャットクライアントのように動作し、コンソールに入力された内容はEnterを押すとサーバーに送信され、サーバーから受信した内容はコンソールにそのまま出力されます。
簡単なSSL echo serverを実装してテストするときに便利です。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop