
しばらくして、同様の内容のエラーログエントリとpcapファイルができました。

サーバーが間違って応答し、クライアントがハンドシェイクを停止したように見えます。 「正しい」(クライアントに受け入れられた)サーバー応答と「誤った」サーバー応答を分析した結果、それらが同一であることがわかりました。
ダンプの分析により、問題はクライアントがTLSチケット(セッション再利用メカニズム)を使用し、チケットがデフォルトキーで暗号化されていない場合にのみ発生することが示されました(この場合、キーがローテーションされる前に受信されましたが、28時間未満です)。
前にも言ったように 、検索は独自のバランサーを使用するため、最初にエラーの検索を開始しました。 ただし、後で問題がクライアントの動作に関連している可能性があることが示唆されました。これは、ブラウザーがWebサーバーへの複数のSSL接続を同時に作成しようとしたときに発生します。 一般的な場合(プリフェッチを忘れるなど)のブラウザ側でのこのような動作(複数の接続)は、次の形式のHTMLを引き起こす可能性があります。
<img src="https://domain.com/x"><img src="https://domain.com/y"><img src="https://domain.com/z">
これらの理論を組み合わせることで、Chromium + Nginxバンチで問題を再現することができ、Balancerコードはそれとは無関係であることがわかりました。 その後、この動作の理由をようやく見つけることができました。
BoringSSLの実装におけるTLSとそのクライアントステートマシンに関する詳細
そのため、既にご存じのとおり、TLSハンドシェイクは長くも短くもなります。
サーバーに最初にアクセスするとき、クライアントの視点からの長いハンドシェイクは次のようになります(特に、わかりやすくするために一部のTLS拡張機能の処理を規定しませんでした)。

SSL3_ST_CRプレフィックス付きの状態-クライアントはSSL3_ST_CWプレフィックス付きのサーバーからメッセージ(レコード)を読み取ります-クライアントはサーバーにメッセージを送信します。 (少し前まで、ChromiumはOpenSSLフォーク(Boringssl)を使用するように切り替えていたため、上記のすべての条件が当てはまります。)
TLSメッセージの構造を見てみましょう。

フィールドの割り当て(一部のTLS拡張機能を省略):
• バージョン -プロトコルのクライアントバージョン(SSL 3.0 / TLS 1.0 / TLS 1.1 / TLS 1.2)、
• ランダム -クライアントランダム、
• セッションIDの長さ -セッションIDフィールドの長さ(最初の呼び出しで0)、
• セッションID-前のセッションの識別子(最初の呼び出しでは空)、
• SessionTicket TLS -TLS拡張、 長さ -拡張のデータ長、 データ値。
(それぞれ、最初の呼び出しで、長さは0および空の値です。)
• 暗号スイート -お客様がサポートする暗号、
• サーバー名 -SNI TLS拡張。これにより、クライアントがアクセスしているドメインをサーバーに通知できます。
次の呼び出しで「高価な」低速のハンドシェイクを行わないために、サーバーはクライアントにセッション再利用の2つの方法のいずれかを使用するように提供できます。 これを行うには、サーバー側に保存されている状態を示すセッションID (RFC 5246)、またはセッションIDとセッションチケットTLS (RFC 5077)をServerHelloのクライアントに返すことができます。 それらについて何度か詳しく話しました 。
RFC 5077は後で登場したため、RFC 5246のセッションメカニズムを補完し、クライアント内の同じ実装を中心に構築されています。 今日は、TLSチケットメカニズムのみを分析します。

フィールドの割り当て:
• バージョン -プロトコルのサーバーバージョン(SSL 3.0 / TLS 1.0 / TLS 1.1 / TLS 1.2)、
• ランダム -サーバーランダム、
• セッションIDの長さ -セッションIDフィールドの長さ(サーバーが新しいチケットを発行する場合、0に設定する必要があります)
• セッションID-前のセッションの識別子(チケットの最初の発行時は0)、
• SessionTicket TLS -TLS拡張。この拡張の存在は、サーバーがクライアントに新しいTLSチケットを発行し、ST_CR_FINISHED_A状態で新しいセッションチケットメッセージを送信し、サーバーをSSL3_ST_CR_SESSION_TICKET_A状態にすることを意味します。

フィールドの割り当て:
• セッションチケットの有効期間のヒント - チケットの有効期間。その後、クライアントはチケットを削除する必要があります(クライアントは、指定された期間0-クライアントの裁量でいつチケットを削除するかを自分で決定できます)。
• セッションチケットの長さ -チケットデータの長さ、
• セッションチケット -チケットの値。
チケットの値とパラメーターはクライアントのメモリに保存されます:

クライアントの場合、チケットの値は無意味なバイナリblobであり、サーバーに送信するか、受信時に保存/更新する必要があることに注意してください。参照フィールドはSession IDです。 サーバーは、チケット値の最初の16バイトを使用して、整合性と復号化の検証に使用されるキーのセットを識別します。 したがって、サーバーはクライアントから古いキーで発行されたチケットを受け入れながら、キー値をローテーションできます。
これは、最初に発行されたチケットを使用した短いハンドシェイクの様子です。

ClientHelloで次の値が設定されます。
• セッションIDの長さ -セッションIDフィールドの長さ(通常32バイト)、
• セッションID -SSL_SESSION構造からのセッションID値、
• SessionTicket TLS -TLS拡張、長さ-チケットデータの長さ、データ-チケット値。
チケットが受け入れられると、サーバーはServerHelloに次のように応答する必要があります。
• セッションIDの長さとセッションIDは、ClientHelloの対応するフィールドと同じです。
さらに、受信したチケットがサーバーによって更新されていない場合(現在のキーが使用されている場合)、ServerHelloのSessionTicket TLSフィールドは存在しません。
チケットがサーバーによって受け入れられたが、キーが変更された場合、ハンドシェイクは次のようになります。

セッションIDの長さとセッションIDは、ClientHelloの対応するフィールドと等しく、 SessionTicket TLSフィールドがServerHelloに追加されました。 これにより、クライアントはSSL3_ST_CR_SESSION_TICKET_A状態になり、新しいセッションチケットメッセージが期待されます。 新しいセッションチケットメッセージを受信すると、クライアントは、ServerHelloからのセッションID値がSSL_SESSIONに格納されているものと等しいことを確認し、 セッション チケット値をSSL_SESSION構造に書き込み、更新します(!) セッションチケット値からのSHA-256ハッシュ関数の結果と等しくなります、状態をSSL3_ST_CR_CHANGEに設定します。
セッションの再利用を担当するChromiumコードの場所は次のようになりました。

ここで、GetSessionCacheKey()は、ドメイン、ポート、プロトコルバージョンを一意に識別します。 つまり、1つのオリジンに対して、シャード内に常に保存されるセッションインスタンスは1つだけです。
SSL_set_session()関数は、セッションインスタンスを指定された接続にコピーしませんが、このインスタンスへのポインターをそれに渡します。
したがって、たとえば3つの接続を連続して初期化する場合、クライアントは同じセッションIDとSessionTicket TLS値を送信します。 最初の接続は成功し、SSL3_ST_CR_SESSION_TICKET_A状態に切り替わります。その後、Session_ID値が変更され、2回目以降はクライアントがServerHelloで空でないセッションIDを受け取り、サーバーから返された値を確認します(クライアントから送信された値と同じ) SSL_SESSION構造体の値と等しくない(最初の接続で既に変更されている)場合、SSL3_ST_CR_CERT_A状態(完全なハンドシェイク)になります。 サーバーは、クライアントが新しいチケット(SSL3_ST_CR_SESSION_TICKET_A)を期待しているとかなり信じて、新しいセッションチケットメッセージを送信します。これは、予期された状態に対応せず、予期しないメッセージアラートにつながります。
この問題は、Yandex.Browser 15.9およびChromium 46で既に修正されています。