これは、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 の値に固定します。(本文が暗号化されたレコードでは、このフィールドを削除しようという議論もあります。)
おわりに
今日は大切なことを説明しました。データ構造を設計する機会がある人は、心に刻んでおきましょう。