あどけない話

Internet technologies

TLS 1.3 開発日記 その3 バージョン

これは、http2 Advent Calendar 2016の7日目の記事です。

今回はTLSのバージョンについて書きます。TLSのバージョンは、Client Hello と Server Hello を交換することで決めます。

Client Hello

TLS 1.3 の Client Hello は、TLS 1.2 と互換性を維持するために、構造が死守されています。

TLS 1.2 の Client Hello の定義はこう:

struct {
    ProtocolVersion client_version;
    Random random;
    SessionID session_id;
    CipherSuite cipher_suites<2..2^16-2>;
    CompressionMethod compression_methods<1..2^8-1>;
    select (extensions_present) {
      case false:
        struct {};
     case true:
        Extension extensions<0..2^16-1>;
    };
} ClientHello;

TLS 1.3 の Client Hello の定義はこう:

struct {
   ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
   Random random;
   opaque legacy_session_id<0..32>;
   CipherSuite cipher_suites<2..2^16-2>;
   opaque legacy_compression_methods<1..2^8-1>;
   Extension extensions<0..2^16-1>;
} ClientHello;

TLS 1.2 では extensions がない場合はまったくバイト列が生成されません。一方、TLS 1.3 では長さ0を表現する 0x0000 が生成されます。しかし、extensionsはほとんど必ず存在するので、その違いを気にする必要はありません。

TLS 1.3 では、session_id や compression_methodsは利用されないので、legacy という接頭辞が付いています。賢明な読者の方ならお気づきでしょうが、version にも legacy が付いています。これはどういうことでしょうか?

TLS1.2でのバージョンの決定

TLS 1.2までは、バージョンを以下のように決定していました。

  • クライアントは Client Hello の version にサポートしている最大のバージョンを入れる
  • サーバは、クライアントが教えてくれた最大のバージョンと、自分がサポートしているバージョンとを照らし合わせて利用するバージョンを決め、その値を Server Hello の server_version に入れる。

これでうまくいきそうですが、それは仕様上の話です。現実の世界では、Client Hello の version を最大値だと思わずに、「そんなバージョン知らない」と言ってコネクションを切ってしまうサーバがたくさん存在しています。その場合、クライアントはバージョンを下げて再び接続を試みる必要があり、応答時間が遅くなるという問題がありました。

TLS1.3でのバージョンの決定

TLS 1.3 では、legacy_version を TLS 1.2 の値に固定します。そして、supported versions という拡張にサポートしているバージョンのリストを入れます。

TLS 1.3クライアントとTLS 1.3サーバの場合:サーバは supported version の中にある TLS 1.3 を選び、Server Hello の version に入れて返します。

TLS 1.3クライアントとTLS 1.2サーバの場合:サーバは supported versionを知らないので無視します。そして、version にある TLS 1.2 を選んで返します。このサーバが、上記のダメなサーバでもうまく動きます。

Server Hello

念のため、Server Hello の構造も確認しておきましょう。

TLS 1.2 の Server Hello はこう:

struct {
    ProtocolVersion server_version;
    Random random;
    SessionID session_id;
    CipherSuite cipher_suite;
    CompressionMethod compression_method;
    select (extensions_present) {
      case false:
        struct {};
      case true:
        Extension extensions<0..2^16-1>;
    };
} ServerHello;

TLS 1.3 の Server Hello はこう:

struct {
    ProtocolVersion version;
    Random random;
    CipherSuite cipher_suite;
    Extension extensions<0..2^16-1>;
} ServerHello;

TLS 1.3 の Server Hello には、session_id がありません。つまり構造が違うのです!

レコード

TLSのメッセージには、Handshake、Alert、Appication Data などがあります。Client Hello や Server Hello は Handshake に属します。これらのデータを本文だと思うと、型や長さを示すヘッダが必要です。このヘッダ+本文の構造のことを TLS ではレコードと呼んでいます。

以下に TLS 1.3 の本文が(暗号化されてない)平文のためのレコードの構造を示します:

struct {
    ContentType type;
    ProtocolVersion legacy_record_version = 0x0301;    /* TLS v1.x */
    uint16 length;
    opaque fragment[TLSPlaintext.length];
} TLSPlaintext;

驚くべきことに、レコードにも TLS のバージョンを示すフィールドがあります。この値と、Client/Server Hello のバージョンの値が食い違ったら、何が起きるでしょうか?

今から大切なことを言いますので、この記事ではこれだけ覚え下さい。

ある1つの値を2つの場所で指示しているなら、それはバグです

バグの例としては、UDPのヘッダに存在する長さフィールドが挙げられます。TCPヘッダには長さフィールドはありませんから、必要ないんです。

legacy_record_version の名前が示すように TLS 1.3 このフィールドを意味がないものとし、中間装置やサーバが最も受け入れてくれそうな、TLS 1.0 の値に固定します。(本文が暗号化されたレコードでは、このフィールドを削除しようという議論もあります。)

おわりに

今日は大切なことを説明しました。データ構造を設計する機会がある人は、心に刻んでおきましょう。