要約

  • TLS 1.3のclose_notifyは、送信者がその接続でこれ以上TLSメッセージを送らないという、認証された一方向の宣言である。反対方向を閉じず、要求、応答、ストリーム、永続的な副作用の受領確認にもならない。
  • アラートなしにトランスポートEOFが来れば、切断による欠落の可能性が残る。上位プロトコルが独立した完全性規則を持ち、アプリケーションが実際に検査した場合に限り、互換動作として扱える。
  • 再試行を認める証拠は、アプリケーションのメッセージ境界、送受それぞれのTLS終了、トランスポート終端、永続結果の四層に分ける必要がある。単一の「closed」では代替できない。

正常終了という誤答

事故記録の末尾にはshutdown=successとあった。運用担当者は応答の配信と取引の完了を意味すると読んだ。しかし、この値を出したのはTLSラッパーであり、ローカルの終了処理しか見ていなかった。

サーバーはコミット前に成功応答を組み立て、TLSレコードを書き、close_notifyを送った。その直後、重複制約によりデータベース更新が拒否された。クライアントが受け取った暗号学的な終端は本物だったが、コミットIDは存在しなかった。

ネットワーク上の順序は、業務上の意味を自動的には得ない。TLSは保護したバイトとアラートを順序づけられる。別プロセスの台帳やキューまで証明する機能はない。

終了は一つの瞬間ではない

TLS 1.3では送信方向ごとに終了する。close_notifyの送信者は、それ以降TLSメッセージを送らない。アラート後に届くデータは無視される。一方、読み取り方向は残り、相手からの最後のデータを受け取れる。

エラーアラートをすでに送った場合を除き、各端点は書き込み側を閉じる前に終了アラートを送る。TLS 1.3が即時応答と保留書き込みの破棄を要求しないのは、受信側がまだ送ろうとしているデータを切り捨てないためである。

TCPのFINにも片方向性があるが、証拠の種類は違う。FINは一方のバイト列の終わりを示すだけで、認証されたTLSアラートではない。完全なTLSレコードやアプリケーションメッセージの終端も特定しない。

EOFで失われる確実性

close_notifyより先に下位トランスポートが消えると、受信者は送信者の予定したデータがすべて届いたか判断できない。プロセス障害、プロキシのタイムアウト、非準拠実装、意図的な切断のいずれでも起こる。攻撃を断定する証拠ではなく、強い終端証拠が欠けた状態である。

致命的アラート、RST、タイムアウト、予期しないEOF、相手の終了アラート、ローカルの送信試行も別々に記録すべきだ。すべてを「disconnected」にすると、使えるメッセージと照合すべき処理を見分けられない。

IANAはアラート値0をclose_notifyに割り当てている。この登録はコードポイントを解釈する根拠であって、実運用の接続がそれを送受信した証拠でも、直前のメッセージが完全だった証拠でもない。

メッセージの終端は上位層が決める

保護されたレコードが終了アラートより前に届けば、その順序は証明できる。ただしTLSにとって内容は不透明であり、全文、最終チャンク、受領票、コミット結果のどれかは分からない。

宣言された長さ、区切り、ゼロ長チャンク、ストリームFIN、最終応答、要求ID、確認応答、永続レコードなど、アプリケーション固有の終端が必要になる。完全な応答が失敗を伝えることも、処理が成功して応答だけ失われることもあるため、境界と結果はさらに分ける。

バッファも中間状態を作る。書き込みAPIの成功は、バイトがTLSやBIOに入っただけかもしれない。ノンブロッキング処理では、アラートの送信試行を記録してから実際にトランスポートへ出るまで間がある。どちらもデータベースの耐久性を保証しない。

OpenSSLの0と1は合否ではない

SSL_shutdown()の戻り値0は通常、ローカルのclose_notifyを送ったが相手のアラートは未受信という状態を表す。エラーではない一方、双方向完了でもない。1になって初めて、両方のアラートを送受信したことになる。

最初の呼び出しで閉じるのはTLS書き込み側で、読み取りは続き、TCPも開いたままである。OpenSSLは最後のアプリケーションデータやハンドシェイク後メッセージを処理するため、相手の終了まで読むことを推奨する。保留データを消費せず再度終了を呼ぶと失敗し得る。

sentフラグは送信試行、receivedフラグは相手のアラートを示す。quiet shutdownはアラートを出さずローカル状態だけ閉じる非準拠動作である。OpenSSL 3.0以降、予期しないEOFには固有のエラー意味がある。SSL_OP_IGNORE_UNEXPECTED_EOFは、上位プロトコルが切断を確実に検出し、その検査を実行する場合にしか正当化できない。

GnuTLSもGNUTLS_SHUT_WRGNUTLS_SHUT_RDWRで送信のみと相手待ちを分ける。BoringSSLも読み書きの終了状態を別管理する。ライブラリに区別があっても、運用指標が一語に潰せば証拠は残らない。

HTTPなら完全性を別に判定できる

HTTP/1.1ではContent-Lengthが指定されればそのバイト数が必要で、chunkedなら最後のゼロ長チャンクが必要である。欠けている場合、接続終了によって完全な応答へ昇格することはない。

接続終了だけで長さを決める応答は特に弱い。TLS終了が不完全なら、HTTPメッセージも不完全になる。RFC 9112が長さまたは符号化による境界を勧めるのは、ネットワーク障害が正常終了に見えるからである。

逆に、所定の長さや終端チャンクをすでに検証したメッセージは、その後に相手のアラートが欠けても境界証拠を保つ。EOF互換を許すなら、利用する各メッセージについてこの独立証拠を示さなければならない。

多重化では要求単位の境界が要る

HTTP/2は一つのTLS接続に複数ストリームを載せる。close_notifyだけでは、サーバーがどの要求の処理を始めたか分からない。GOAWAYの最終ストリームIDが、処理された可能性のある範囲を与える。

GOAWAYなしで接続が終わると、飛行中の非冪等POSTは曖昧なままである。HTTP/3もQUIC上でGOAWAYを使い、受理した要求範囲を示す。接続終了から要求単位の確認応答を推論することはできない。

一律に再試行すれば二重決済が起こり、一律に止めれば処理が失われる。メソッドの意味、冪等キー、結果照会、照合処理が再試行権限を与える。

QUICは別の終了語彙を持つ

QUICはTLSハンドシェイクを使うが、アプリケーションデータをTLSレコードで保護しない。TLSアラートはQUIC接続エラーに変換され、QUIC自身がCONNECTION_CLOSE、ストリームFIN、reset、closing、drainingを定義する。close_notifyのwarning終了を持ち込めない。

HTTP/3がさらにGOAWAYを必要とすることも、接続終端だけでは要求範囲が決まらない証拠である。指標にはTLSアラート、TCP FIN/RST、QUICストリームFIN、CONNECTION_CLOSE、アイドルタイムアウト、HTTP GOAWAY、アプリ確認のどれかを明記する。

終了の証拠台帳を作る

端点の役割、方向、TLS版、接続相関ID、最後の完全なメッセージまたはストリームを残す。フレーミング規則、予定と実受信のバイト数、要求ID、冪等キー、再試行区分も必要である。

TLSについては、ローカルアラートのキュー投入と送出、相手アラートの受信、ライブラリ戻り値列、読み書き状態、保留中の平文と暗号文、正確なエラー分類を分ける。quiet、EOF互換、ライブラリ版、kTLS、プロキシ境界も記録する。

トランスポートではFIN、RST、EOF、タイムアウト、半閉鎖を区別し、多重化プロトコルではGOAWAY範囲とストリーム終端を保つ。業務層では確認、コミットID、永続時刻、その発行主体を保存する。

負の試験には、アラート前のTCP切断、終端チャンク欠落、完全フレーム後かつコミット前の閉鎖、最初の戻り値0、未読データ、quiet shutdown、EOF方針切替、非冪等要求を抱えたHTTP/2・HTTP/3切断を含める。

出典