あどけない話

Internet technologies

TLS レコード・サイズ制限拡張

tlsfuzzer

僕が tlsfuzzer を知ったのは、知人からのメールがきっかけだった。tlsfuzzerは、fuzzer という名前には反して、対象であるTLSの中身を高度に理解して実装されている強力なテストツールである。各テスト項目が、それぞれのPythonスクリプトとして提供されいる。インストールは簡単で、Python初心者の僕でも気軽に利用できた。

彼女は、Haskell tls ライブラリが、テストの1つに失敗することを指摘してくれた。TLS 1.3では Finished は暗号化されているが、平文の Finished を送っても、ハンドシェイクが受理されるというバグが存在したのだ。

issueを登録するようにお願いしたにも関わらず、忙しくて放置してしまった。約2年後、心にひっかかっていたバグをようやく修正して、ついでに tlsfuzzer が提供する他のテストも試し始めた

tlsfuzzerのテストを通すには、Pythonスクリプトに適切な引数を渡さなければならない場合がある。この作業では、その引数を記録することを怠ったため、テストの成功を再現するのが困難になってしまった。他のプロジェクトが忙しくなったこともあり、tlsfuzzerによるテストをこの時もまた放置してしまった。

時は経ち Haskell tlsライブラリの保守を単独で引き継いだ僕は、ライブラリを自由に修正できるようになった。そこで、IETFにより「廃止」された SSL 3、TLS 1.0、TLS 1.1、そしてストリーム暗号やCBCモードのブロック暗号のコードを削除した。後方互換性を意図的に破壊することで、ビルドで失敗するようになった Haskeller に「モダンで安全なTLSに移行しましょう」というメッセージを送ったのだ。ただし、凝った設定をしていなければビルドは成功するので、自動的に安全性が高まる。

昨年の12月に余裕ができたので、tlsfuzzerへ再び取り組んでみることにした。tlsfuzzer のスクリプトの多くは、脆弱な暗号方式や非推奨のパラメータを要求しており、以前通ったはずのテストも通らなくなっていた。そこで、専用ブランチを作って、それらを復活させた。

前回の反省を活かし、通ったテストに関してはコマンド引数をシェルスリプトとして保存し、回帰(regression)テストとして利用できるようにした。このシェルスリプトは強力な武器となり、これ以降、何度救われたか分からない。Haskell内のテストでは、クライアントとサーバが同じロジックを共有しているので、お互いが同じように間違って、テストが成功してしまう可能性がある。この回帰テストのおかげで、大胆な変更に対しても自信を持てるようになった。

レコード・サイズ制限拡張

tlsfuzzerには、Haskell tls がまだ実装していない署名圧縮拡張レコード・サイズ制限拡張を対象としているスクリプトもあった。モチベーションが高まったので、これらを実装してみた。人が書いたテストを活用できるのは、本当にありがたい。

署名圧縮拡張に関しては特に書くことはないが、レコード・サイズ制限拡張の仕様は少し複雑なので、理解している内に記録に残しておこうと思う。

基本的なレコード・サイズとは:

  • TLS 1.2において、平文の最大レコード・サイズは214(16,384)である。暗号文の場合は、暗号文のオーバーヘッドに従って、これより大きなサイズを格納してよい。
  • TLS 1.3においても、平文の最大レコード・サイズは214(16,384)である。暗号文のオーバヘッドには、暗号自体に加えて、平文に加えられるContent-Typeやパディングもある。Content-Typeのサイズは1で、パディング長は0でもよい。

レコード・サイズ制限拡張で提案するレコード・サイズとは:

  • 基本的な利用方法は、レコード・サイズの上限を引き下げることである。
  • TLS 1.2 に対し提案するレコード・サイズは、平文の大きさの上限である。
  • TLS 1.3 に対し提案するレコード・サイズは、平文の大きさの上限 - 1 である。これは、Content-Typeも含むと定義されているからである。
  • クライアントが TLS 1.2 と TLS 1.3 の両方を提案する場合、サーバがどちらを選ぶかはあらかじめ分からない。TLS 1.3 が選ばれた場合、値が -1 されることに注意しないといけない。(ここがダサい。)

合意形成:

  • レコード・サイズ制限拡張に対応したクライアントは、ClientHelloに拡張を入れて送る。
  • レコード・サイズ制限拡張に対応したサーバは、クライアントからレコード・サイズ制限拡張が送られてきた場合に限り、レコード・サイズ制限拡張を返す。
  • 双方がレコード・サイズ制限拡張を送った場合に限り、レコード・サイズ制限拡張で指定されている上限が有効になる。
  • 両者が送るレコード・サイズは異なってよい。クライアントが送った上限は、サーバがレコードを送信する際に利用される。逆もまた然り。

提案するレコード・サイズの最小値:

  • 提案してよいレコード・サイズの最小値は64である。これより小さい値を受け取った場合は、alert を返す。

提案するレコード・サイズの最大値:

  • 提案してよいレコード・サイズの最大値は、他の拡張で許されない限り、プロトコルで定義されている最大値である。つまり、TLS 1.2 では 214TLS 1.3 では 214 + 1 である。TLS 1.2も提案する場合、実質的に 214が最大値となる。(以下で説明するように 214 + 1 を提案する裏技もある。)
  • サーバが、プロトコルで定義されている上限よりも大きな値を受け取った場合、受理するが、送信時にはプロトコルで定義されている上限を利用する。alert を返してはならない。なぜなら、クライアントが未知の拡張を使って上限が引き上げようとしているかもしれないからである。(ここがすごい)

当初、僕の仕様に対する理解が不完全であったが、tlsfuzzerがエラーを発して間違った理解を指摘してくれた。感謝!