Hatena::ブログ(Diary)

あどけない話

2016-12-08

TLS 1.3 開発日記 その4 フルハンドシェイク

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

今回はTLS 1.3のフルハンドシェイクについて書きます。

TLS 1.2のフルハンドシェイク

おさらいとして、RFC5246からTLS 1.2のフルハンドシェイクの図を少し変更して抜粋します。角カッコは暗号化されていることを意味します。

Client                                               Server

      ClientHello                  -------->
                                                      ServerHello
                                                      Certificate
                                                ServerKeyExchange
                                   <--------      ServerHelloDone
      ClientKeyExchange
      ChangeCipherSpec
      [Finished]                   -------->
                                                 ChangeCipherSpec
                                   <--------           [Finished]
      [Application Data]           <------->   [Application Data]

TLS 1.3 と比較する意味で、特筆すべき点を挙げます。

  • (EC)DHEの鍵交換は、サーバ側から始まります。ServerKeyExchangeにはサーバの(EC)DHE公開鍵の他に、証明書に対応する(RSAなどの)秘密鍵署名した署名が入っています。これにより、クライアントサーバを認証できます。
  • ServerHelloDone は、ServerHelloシリーズが終わる空の目印で、認証等の役割はありません。
  • 暗号化が始まる(正確には切り替わる)ことを告げる ChangeCipherSpec を送ります。定義としては、これは Handshake ではなく、独立な型を持つメッセージです。
  • クライアントからの Application Data は、Finished に送ることも可能です(1RTT)。しかし、サーバからの Application Data の送信には必ず 1.5RTT かかります。

なお、Finishedは、これまでのハンドシェイクに対して、共有した鍵を使って HMAC を計算し、共有鍵および改ざんがないことを確認します。

TLS1.3 のフルハンドシェイク

TLS 1.3のドラフトからTLS 1.3のフルハンドシェイクの図を少し変更して抜粋します。波カッコは初期の共有鍵で、角カッコは後期の共有鍵で暗号化されていることを示します。

       Client                                               Server

Key  ^ ClientHello
Exch | + supported_versions
     | + key_share
     v + signature_algorithms    -------->
                                                       ServerHello  ^ Key
                                                      + key_share   v Exch
                                             {EncryptedExtensions}  - Params
                                                     {Certificate}  ^
                                               {CertificateVerify}  | Auth
                                                        {Finished}  v
                                 <--------     [Application Data*]
Auth - {Finished}                -------->
       [Application Data]        <------->      [Application Data]

特筆すべき点は以下の通りです。

  • 1.3以降のバージョンを決定するために Client Hello に supported_version 拡張があります。
  • (EC)DHE の鍵交換は、key_share という拡張を使います。クライアントから始まることに注意して下さい。
  • クライアントサーバの証明書を使ってサーバを認証する場合、signature_algorithms拡張の送信が必須となってます。サーバ認証の値が、暗号スイートに含まれないからですね。
  • Server Hello で返すほとんどの拡張は、暗号化されている Encrypted Extensions に入ります。たとえば、ALPN で何のプロトコルを選んだかは、暗号化される訳です。
  • CertificateVerify は、Certificate に対応する秘密鍵で生成した署名です。サーバを認証するのが目的です。TLS 1.2 とは違い、鍵交換とは明確に切り離されています。
  • Finishedは初期の共有鍵で暗号化されています。
  • サーバは Finished を返した直後から、Application Data を暗号化して返せます(0.5RTT)。HTTP通信クライアントから始まりますが、メールのプロトコルサーバ側から始まることが多いので、TLS 1.2 と顕著な差が出ます。クライアントからの Application Data の送信は、このハンドシェイクでは1RTTです。

2016-12-07

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 の値に固定します。(本文が暗号化されたレコードでは、このフィールドを削除しようという議論もあります。)

おわりに

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

2016-12-03

TLS 1.3 開発日記 その2 暗号スイート

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

今回は暗号スイートについて書きます。TLS 1.2 の暗号スイートは、たとえば以下のような感じでした。

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

これは次のような意味です。

TLS 1.1 以前だと、この暗号スイートのハッシュの意味が異なるんですが、面倒なので説明は割愛します。

TLS 1.3だと、暗号スイートが以下のようになります。

TLS_AES_128_GCM_SHA256

これは以下のような意味です。

暗号スイートは、TLS 1.2 と同様に、Client Hello でリストが提示され、Server Hello で選択されます。

さて、鍵交換とサーバ認証はどこにいってしまったのでしょうか?

TLS 1.3 の鍵交換

TLS 1.3 の鍵交換は、Client Helloのkey share拡張で提示されます。

struct {
    NamedGroup group;
    opaque key_exchange<1..2^16-1>;
} KeyShareEntry;

struct {
    select (Handshake.msg_type) {
        case client_hello: KeyShareEntry client_shares<0..2^16-1>;
        case hello_retry_request: NamedGroup selected_group;
        case server_hello: KeyShareEntry server_share;
    };
} KeyShare;

この中に、P256なECDHEの公開鍵やX25519なECDHEの公開鍵が入るわけです。見ての通り、サーバはその中から1つを選び対応する公開鍵をServer Helloに1つだけ入れます。

TLS 1.3のサーバ認証

TLS 1.3 のサーバ認証は、Client Helloのsignature algorithms拡張で提示されます。

enum {
    /* RSASSA-PKCS1-v1_5 algorithms */
    rsa_pkcs1_sha1(0x0201),
    rsa_pkcs1_sha256(0x0401),
    rsa_pkcs1_sha384(0x0501),
    rsa_pkcs1_sha512(0x0601),

    /* ECDSA algorithms */
    ecdsa_secp256r1_sha256(0x0403),
    ecdsa_secp384r1_sha384(0x0503),
    ecdsa_secp521r1_sha512(0x0603),

    /* RSASSA-PSS algorithms */
    rsa_pss_sha256(0x0804),
    rsa_pss_sha384(0x0805),
    rsa_pss_sha512(0x0806),

    /* EdDSA algorithms */
    ed25519(0x0807),
    ed448(0x0808),

    /* Reserved Code Points */
    private_use(0xFE00..0xFFFF),
    (0xFFFF)
} SignatureScheme;

struct {
    SignatureScheme supported_signature_algorithms<2..2^16-2>;
} SignatureSchemeList;

そして、Server Certificate Verifyで何が選択されたかが表明されます。

struct {
    SignatureScheme algorithm;
    opaque signature<0..2^16-1>;
} CertificateVerify;

おまけ

どうして、鍵交換とサーバ認証が暗号スイートから切り離されたか疑問に思うことでしょう。その理由は、そのうち説明します。つまり日記はまだまだ続きます。

2016-12-01

TLS 1.3 開発日記 その1

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

現在、IETFTLS 1.3 の標準化が大詰めを迎えています。僕も TLS 1.3 の標準化に参加しており、仕様の分かりにくい部分を直したり、TLS 1.3 を Haskell で実装したりしています。この開発日記のシリーズでは、TLS 1.3の仕組みを説明していこうと思います。

そもそも、なぜ TLS 1.3 が必要なのかは、TLSの動向をお読み下さい。なお、次のTLSのバージョンを何にするかは、現在もめていて、1.3ではなくなる可能性もあることに注意して下さい。

現在の実装

現在利用できる TLS 1.3 の実装の一覧は、Implementationsにまとまっています。僕がクライアントとしてよく使っているのは、Firefox Nightly、Chrome Canary、および picotls です。

TLS 1.3 を気軽に試したいなら、Firefox Nightly や Chrome Canary をインストールし、Implementationsに書かれている設定をした後に、CouldFlare の TLS 1.3 テストサーバにアクセスしてみるといいでしょう。ページが表示されると、TLS 1.3 でアクセスできたことになります。

Implementationsには、Wiresharkも掲載されていますが、実際にはTLS 1.3に対応していません。tcpudmp などでキャプチャしたパケットを表示できるようになるには、時間がかかるかもしれません。

Haskellの実装

僕は Haskell コミュニティで以下のような役割を持っています。

過去2年ぐらいの間、WarpHTTP/2に対応させてきました。その成果は、Experience Report: Developing High Performance HTTP/2 Server in Haskellという論文にまとまっています。また、TLSに関しては、足りない機能を実装してきました。

僕の次の目標は、HaskellTLSライブラリTLS 1.3の機能を組み込むことです。(それが終わったら、QUICを実装したいと小声で言っておきます。)

TLS 1.3の開発は、Haskell暗号ライブラリである cryptonite を拡張することから始まりました。cryptoniteには、Haskellで書かれた楕円曲線暗号があります。これを使って、HTTP/2に必要な ECDHE を実装しマージしてもらっていました。

しかし、Haskellで書かれた楕円曲線暗号は遅いという問題があり、Cで書かれたP256やCで書かれたX25519をバックエンドとして使えるように、抽象化レイヤを設け、その上に ECDHE を再実装しました。確か4回ぐらい再実装したと思います。現在、その成果はレビュー中です。

次に、TLSライブラリ自体を拡張しました。現時点は、TLS 1.3のサーバ側のみ実装できており、4つのハンドシェイクモードすべてに対応しています。Firefox Nightly、Google Chrome、および picotls と相互接続性を確認しています。また、この TLS ライブラリを使って、Web アプリケーションが実際に動くことも確認しています。

TLS 1.3の実装を通じて驚嘆するのは、仕様の美しさです。理解すればする程、よく考えらえていると感じます。この開発日記では、その美しさの片鱗でも説明できればよいなと思います。

では、また次の記事でお会いしましょう。

2016-01-14

重複したフィールドラベル

Haskell 2010 では、同じファイルに重複したフィールドラベルを定義できない。たとえば、以下はエラーになる。

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float } -- これはダメ

この問題を解決する案は、OverloadedRecordFields と呼ばれ、苦難の歴史を持つ。実装があるにもかかわらず、

  • 実装が一枚岩
  • コードの複雑になる割に利益が少ない

などの理由により、GHC へはマージされずにいた。現在では、OverloadedRecordFieldsは、三つの拡張へと分割された:

  1. DuplicateRecordFields
  2. OverloadedLabels
  3. Magic type classes

この中、1. と 2. が GHC 8.0 に入る。

DuplicateRecordFields

DuplicateRecordFields は単純で、同一ファイルで重複したフィールドラベルが「定義」されることを許す。以下は、GHC 8.0 ではエラーにならない。

{-# LANGUAGE DuplicateRecordFields #-}

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float }

しかし、フィールドラベルを使おうとするとエラーになる。

{-# LANGUAGE DuplicateRecordFields #-}

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float }

main :: IO ()
main = do
    let x = Foo 1
    print $ same x -- これはダメ

型推論はどこで完了するか分からないので、型を取ってこれないらしい。型を明記すれば使える。

{-# LANGUAGE DuplicateRecordFields #-}

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float }

main :: IO ()
main = do
    let x = Foo 1
    print $ same (x :: Foo)

これはやってられない感じがする。

OverloadedLabels

この問題を解決するための第一歩が OverloadedLabels である。とっても面倒だが、以下のような記述で、# が付いたフィールドラベルが使えるようになる。

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE DataKinds, TypeFamilies #-}
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}

import GHC.OverloadedLabels

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float }

instance (a ~ Int) => IsLabel "same" (Foo -> a) where
  fromLabel _ (Foo n) = n

instance (a ~ Float) => IsLabel "same" (Bar -> a) where
  fromLabel _ (Bar d) = d

main :: IO ()
main = do
    let x = Foo 1
    print $ #same x

将来の GHC では、このボイラープレートが自動導出されるようになるそうだ。