web-dev-qa-db-ja.com

ブーストasio ssl async_shutdownは常にエラーで終了しますか?

私はboost 1.55 asioでプログラムした小さなsslクライアントがあり、boost::asio::ssl::stream::async_shutdown()が常に失敗する理由を理解しようとしています。クライアントは、boost::asio::ip::tcp::resolver::async_resolve()-> boost::asio::ssl::stream::async_connect()-> boost::asio::ssl::stream::async_handshake()を経由するという点で、boostドキュメントのsslクライアントの例と非常に似ています(ほぼ同じ)。コールバックシーケンス。これはすべて期待どおりに機能し、async_handshake()コールバックは完全にクリアな_boost::system::error_code_を取得します。

async_handshake()コールバックからasync_shutdown()を呼び出します(データは転送しません-このオブジェクトはハンドシェイクのテスト用です):

_void ClientCertificateFinder::handle_handshake(const boost::system::error_code& e)
{
    if ( !e )
    {
        m_socket.async_shutdown( boost::bind( &ClientCertificateFinder::handle_shutdown_after_success, 
            this, 
            boost::asio::placeholders::error ) );
    }
    else
    {
        m_handler( e, IssuerNameList() );
    }
}
_

次にhandle_shutdown_after_success()が呼び出されますが、常にエラーが発生しますか?エラーは_asio.misc_のvalue = 2で、これは「ファイルの終わり」です。私はさまざまなsslサーバーでこれを試しましたが、常にこの_asio.misc_エラーが発生するようです。これが根本的なopensslエラーではないことは、何らかの形でasioを誤用している可能性があることを示唆しています...?

なぜこれが起こっているのか誰でも知っていますか?私はasync_shutdown()との接続をシャットダウンすることが正しいことだと思っていましたが、boost::asio::ssl::stream.lowestlayer().close()を呼び出すだけで、opensslの下からソケットを閉じることができると思いますこれを行うための予想される方法(そして実際、asio sslの例は、これがシャットダウンの正しい方法であることを示しているようです)。

29
Ted Middleton

暗号化された安全なシャットダウンの場合、両者はshutdown()またはasync_shutdown()を呼び出してboost::asio::ssl::streamを実行することにより、 io_service に対してシャットダウン操作を実行する必要があります。 SSLカテゴリ のないerror_codeで操作が完了し、シャットダウンの一部が発生する前にキャンセルされなかった場合、接続は安全にシャットダウンされ、基になるトランスポートが再利用またはクローズされます。最下層を単に閉じると、セッションが 切り捨て攻撃 に対して脆弱になる可能性があります。


プロトコルとBoost.Asio API

標準化された [〜#〜] tls [〜#〜] プロトコルおよび非標準化された SSLv プロトコルでは、安全なシャットダウンには、パーティがclose_notifyメッセージを交換する必要があります。 Boost.Asio APIに関しては、どちらかの当事者がshutdown()またはasync_shutdown()を呼び出すことでシャットダウンを開始し、close_notifyメッセージを他の当事者に送信して、受信者にイニシエーターは、SSL接続でこれ以上メッセージを送信しません。仕様に従って、受信者はclose_notifyメッセージで応答する必要があります。 Boost.Asioはこの動作を自動的に実行せず、受信者がshutdown()またはasync_shutdown()を明示的に呼び出す必要があります。

この仕様では、シャットダウンの開始者がclose_notify応答を受信する前に接続の読み取り側を閉じることができます。これは、アプリケーションプロトコルが基になるプロトコルを再利用したくない場合に使用されます。残念ながら、Boost.Asioは現在(1.56)この機能を直接サポートしていません。 Boost.Asioでは、shutdown()操作は、エラーが発生した場合、またはパーティがclose_notifyメッセージを送受信した場合に、完了したと見なされます。操作が完了すると、アプリケーションは基になるプロトコルを再利用するか、閉じることができます。

シナリオとエラーコード

SSL接続が確立されると、シャットダウン中に次のエラーコードが発生します。

  • 1つのパーティがシャットダウンを開始し、リモートパーティがプロトコルをシャットダウンせずに、基になるトランスポートを閉じるか、すでに閉じています:
    • イニシエーターのshutdown()操作は、SSLの短い読み取りエラーで失敗します。
  • 一方の当事者がシャットダウンを開始し、リモートの当事者がプロトコルをシャットダウンするのを待ちます:
    • イニシエーターのシャットダウン操作はboost::asio::error::eofのエラー値で完了します。
    • リモートパーティのshutdown()操作が正常に完了しました。
  • 一方の当事者がシャットダウンを開始し、リモートの当事者がプロトコルをシャットダウンするのを待たずに、基礎となるプロトコルを閉じます。
    • イニシエーターのshutdown()操作はキャンセルされ、boost::asio::error::operation_abortedのエラーが発生します。これは、以下の詳細に記載されている回避策の結果です。
    • リモートパーティのshutdown()操作が正常に完了しました。

これらのさまざまなシナリオを以下で詳しく説明します。各シナリオはスイムラインのような図で示され、各当事者がまったく同じ時点で何をしているのかを示しています。

PartyA 後にshutdown()を呼び出します PartyB シャットダウンをネゴシエートせずに接続を閉じます。

このシナリオでは、 PartyB 最初にストリームでshutdown()を呼び出さずに基礎となるトランスポートを閉じることにより、シャットダウン手順に違反します。基になるトランスポートが閉じられると、 PartyAshutdown()を開始しようとします。

 PartyA                              | PartyB
-------------------------------------+----------------------------------------
 ssl_stream.handshake(...);          | ssl_stream.handshake(...);
 ...                                 | ssl_stream.lowest_layer().close();
 ssl_stream.shutdown();              |

PartyAclose_notifyメッセージを送信しようとしますが、基になるトランスポートへの書き込みはboost::asio::error::eofで失敗します。 Boost.Asioは 明示的にマップ 基になるトランスポートのeofエラーをSSLの短い読み取りエラーに変換します。 PartyB SSLシャットダウン手順に違反しています。

if ((error.category() == boost::asio::error::get_ssl_category())
     && (ERR_GET_REASON(error.value()) == SSL_R_SHORT_READ))
{
  // Remote peer failed to send a close_notify message.
}

PartyA 次にshutdown()を呼び出します PartyB シャットダウンをネゴシエートせずに接続を閉じます。

このシナリオでは、 PartyA シャットダウンを開始します。ただし、 PartyBclose_notifyメッセージを受け取り、 PartyB 基になるトランスポートを閉じる前にshutdown()で明示的に応答しないことにより、シャットダウン手順に違反します。

 PartyA                              | PartyB
-------------------------------------+---------------------------------------
 ssl_stream.handshake(...);          | ssl_stream.handshake(...);
 ssl_stream.shutdown();              | ...
                                     | ssl_stream.lowest_layer().close();

Boost.Asioのshutdown()操作は、close_notifyが送受信された後、またはエラーが発生したときに完了したと見なされるため、 PartyAclose_notifyを送信し、応答を待ちます。 PartyB SSLプロトコルに違反して、close_notifyを送信せずに基礎となるトランスポートを閉じます。 PartyAの読み取りはboost::asio::error::eofで失敗し、Boost.AsioはそれをSSLの短い読み取りエラーにマップします。

PartyAshutdown()を開始して待機します PartyBshutdown()で応答します。

このシナリオでは、 PartyA シャットダウンを開始し、待機します PartyB シャットダウンで応答します。

 PartyA                              | PartyB
-------------------------------------+----------------------------------------
 ssl_stream.handshake(...);          | ssl_stream.handshake(...);
 ssl_stream.shutdown();              | ...
 ...                                 | ssl_stream.shutdown();

これはかなり基本的なシャットダウンであり、両者はclose_notifyメッセージを送受信します。シャットダウンが両方の当事者によってネゴシエートされると、基になるトランスポートは再利用またはクローズされます。

  • PartyAのシャットダウン操作はboost::asio::error::eofのエラー値で完了します。
  • PartyBのシャットダウン操作は正常に完了します。

PartyAshutdown()を開始しますが、待機しません PartyB 応答する。

このシナリオでは、 PartyA シャットダウンを開始し、close_notifyが送信されるとすぐに基になるトランスポートを閉じます。 PartyA 待っていない PartyBclose_notifyメッセージで応答します。このタイプのネゴシエートされたシャットダウンは、仕様に従って許可され、実装間でかなり一般的です。

上記のように、Boost.Asioはこのタイプのシャットダウンを直接サポートしていません。 Boost.Asioのshutdown()操作は、リモートピアがclose_notifyを送信するのを待ちます。ただし、仕様を維持しながら回避策を実装することは可能です。

 PartyA                              | PartyB
-------------------------------------+---------------------------------------
 ssl_stream.handshake(...);          | ssl_stream.handshake(...)
 ssl_stream.async_shutdown(...);     | ...
 const char buffer[] = "";           | ...
 async_write(ssl_stream, buffer,     | ...
  [](...) { ssl_stream.close(); })   | ...
 io_service.run();                   | ...
 ...                                 | ssl_stream.shutdown();

PartyA 非同期シャットダウン操作を開始してから、非同期書き込み操作を開始します。書き込みに使用されるバッファーは、ゼロ以外の長さでなければなりません(上記ではヌル文字が使用されています)。それ以外の場合、Boost.Asioは、no-opへの書き込みを最適化します。 shutdown()操作が実行されると、close_notifyが PartyB、SSLの書き込み側を閉じる PartyAのSSLストリーム、および非同期的に待機 PartyBclose_notifyです。ただし、書き込み側として PartyAのSSLストリームが閉じました。async_write()操作は、プロトコルがシャットダウンされたことを示すSSLエラーで失敗します。

if ((error.category() == boost::asio::error::get_ssl_category())
     && (SSL_R_PROTOCOL_IS_SHUTDOWN == ERR_GET_REASON(error.value())))
{
  ssl_stream.lowest_layer().close();
}

失敗したasync_write()操作は、基になるトランスポートを明示的に閉じ、待機しているasync_shutdown()操作を引き起こします。 PartyBキャンセルされるのclose_notify

  • でも PartyA SSL仕様で許可されているシャットダウン手順を実行し、基になるトランスポートが閉じられたときにshutdown()操作が明示的にキャンセルされました。したがって、shutdown()操作のエラーコードの値はboost::asio::error::operation_abortedになります。
  • PartyBのシャットダウン操作は正常に完了します。

要約すると、Boost.AsioのSSLシャットダウン操作は少し注意が必要です。適切なシャットダウン中のイニシエータとリモートピアのエラーコード間の不一致により、処理が少し厄介になることがあります。原則として、エラーコードのカテゴリがSSLカテゴリでない限り、プロトコルは安全にシャットダウンされました。

52
Tanner Sansbury