0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby で ECH のサーバテストツールを実装した話

Posted at

これは、Ruby Advent Calendar 2024 の 16 日目の記事です。

はじめに

本記事では、Ruby で ECH のサーバテストツールを実装した話について書きます。

ECH の概要

2024 年 12 月現在、IETF の TLS WG において ECH (Encrypted Client Hello) という TLS 拡張が議論されています。

TLS は、ハンドシェイクメッセージの Client Hello に通信先のホスト名を記載します。このメッセージは平文で送信されます。第三者は、Client Hello を観測することでクライアントがアクセスしている Web サイトを特定できます。ECH は、Client Hello を暗号化することで第三者から読み取れないようにする拡張です。

ECH のテストツール echspec

ECH は新しい TLS 拡張であり、さまざまなプログラミング言語で実装が進んでいます。新しい仕様において、異なる実装間の動作や互換性を確認することは非常に重要です。そこで、サーバ実装が仕様に沿った動作をするかどうかをテストするツール echspec を実装することにしました。

echspec は TLS 1.3 のクライアントとして動作し、サーバとハンドシェイクを試みます。このハンドシェイクの過程で、サーバが ECH の仕様に沿った実装になっているかどうかをテストします。

例えば、ECH のドラフトには以下の記述があります。

First it parses EncodedClientHelloInner, interpreting all bytes after client_hello as padding. If any padding byte is non-zero, the server MUST abort the connection with an "illegal_parameter" alert.

https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-5.1-9

詳細は省きますが、クライアントから送信されるメッセージの 0 パディング部分が 0 以外でパディングされていた場合、サーバは illegal_parameter アラートを返してハンドシェイクを中止する必要があります (MUST) 。echspec は、この部分を 0 以外でパディングしてサーバに送信し illegal_parameter アラートが返送されるかどうかをテストします。echspec は、このような仕様に基づくサーバの振る舞いをテストケースとして検証します。

echspec の実行例

下記は実行例です。

$ echspec research.cloudflare.com
TLS Encrypted Client Hello Server
        ✔ MUST implement the following HPKE cipher suite: KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256 and AEAD: AES-128-GCM. [9]
        ✔ MUST abort with an "illegal_parameter" alert, if EncodedClientHelloInner is padded with non-zero values. [5.1-9]
        ✔ MUST abort with an "illegal_parameter" alert, if any referenced extension is missing in ClientHelloOuter. [5.1-10]
        ✔ MUST abort with an "illegal_parameter" alert, if any extension is referenced in OuterExtensions more than once. [5.1-10]
        ✔ MUST abort with an "illegal_parameter" alert, if "encrypted_client_hello" is referenced in OuterExtensions. [5.1-10]
        ✔ MUST abort with an "illegal_parameter" alert, if the extensions in ClientHelloOuter corresponding to those in OuterExtensions do not occur in the same order. [5.1-10]
        ✔ MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloInner. [7-5]
        x MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloOuter. [7-5]
        ✔ MUST abort with an "illegal_parameter" alert, if ClientHelloInner offers TLS 1.2 or below. [7.1-11]
        ✔ MUST include the "encrypted_client_hello" extension in its EncryptedExtensions with the "retry_configs" field set to one or more ECHConfig. [7.1-14.2.1]
        ✔ MUST abort with a "missing_extension" alert, if 2nd ClientHelloOuter does not contains the "encrypted_client_hello" extension. [7.1.1-2]
        ✔ MUST abort with an "illegal_parameter" alert, if 2nd ClientHelloOuter "encrypted_client_hello" enc is empty. [7.1.1-2]
        ✔ MUST abort with a "decrypt_error" alert, if fails to decrypt 2nd ClientHelloOuter. [7.1.1-5]

Failures:

        1) MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloOuter. [7-5]
                did not send expected alert: illegal_parameter

1 failure

ECH は、Client Hello を暗号化します。そのため、Wireshark を用いてもクライアントが送信した Client Hello を読むことは難しいです。2024 年 12 月現在、ECH を復号するための SSLKEYLOGFILE 拡張はドラフト段階です。

echspec は、失敗したテストケースのハンドシェイクメッセージやアラートメッセージを JSON フォーマットで出力できます。下記のように、jq を使用してテスト結果であるメッセージのスタックを確認できます。

$ echspec research.cloudflare.com -v 2>&1 > /dev/null |  jq .
{
  "Alert": {
    "level": "0x02",
    "description": "0x32"
  },
  "ClientHello": {
    "msg_type": "0x01",
    "legacy_version": "0x0303",
    "random": "0x07091fa0eec43ccc4158b1514998092b4dda0303b238ad51867d355651aa1377",
    "legacy_session_id": "0x1679fc195436817295f55b4bba927826d7f26c0ac6b936d2ea69692a3c00f418",
    "cipher_suites": [
      "0x1302",
      "0x1303",
      "0x1301"
    ],
    "legacy_compression_methods": [
      "0x00"
    ],
    "extensions": {
      "0x0000": {
        "extension_type": "0x0000",
        "server_name": "0x636c6f7564666c6172652d6563682e636f6d"
      },
      "0x002b": {
        "extension_type": "0x002b",
        "msg_type": "0x01",
        "versions": [
          "0x0304"
        ]
      },
      "0x000d": {
        "extension_type": "0x000d",
        "supported_signature_algorithms": [
          "0x0403",
          "0x0503",
          "0x0603",
          "0x0804",
          "0x0805",
          "0x0806",
          "0x0401",
          "0x0501",
          "0x0601"
        ]
      },
      "0x000a": {
        "extension_type": "0x000a",
        "named_group_list": [
          "0x0017",
          "0x0018",
          "0x0019"
        ]
      },
      "0x0033": {
        "extension_type": "0x0033",
        "msg_type": "0x01",
        "key_share_entry": [
          {
            "group": "0x0017",
            "key_exchange": "0x04e08b2ff2039880820a03a9706661ca6351bb1b4f03befb011478d18a84acee6caf99bcdbc15434038ccbeb2eac727dc0e223c173d0ae30510679131de0a28937"
          },
          {
            "group": "0x0018",
            "key_exchange": "0x04646da67d01eb822c3438f73c23cc1f110d327798ed494b0f3e80f0f610da9e23ee7c40ba0416e58b6ae7b07c144aed35d81aead168a24488f028894d2fad3507deb22fdbff1fadf2a52e99e44596315bc43d99d1bd14be334e6de522282e86b3"
          },
          {
            "group": "0x0019",
            "key_exchange": "0x04012e6c12fc453a168987c0188b328c9e3c0da51761b5f7c2dc2a91037ca80771d5b5f434c9070bb3103e966fb0528409152b5ab65c9bb4ac2d21801dd4c70cd88028003a5faef2b03632cd956987ed9e1e9e6cb38a94f5c82a97826788d3a8ba0ce9477f82bcfc56adf2bc57d1abdbbd1602b401465f7e344ed682d30d2f6e3d7aa955c9"
          }
        ]
      },
      "0xfe0d": {
        "extension_type": "0xfe0d",
        "type": "0x02",
        "cipher_suite": {
          "kdf_id": {
            "uint16": 1
          },
          "aead_id": {
            "uint16": 1
          }
        },
        "config_id": 8,
        "enc": "0x6a618d214da5991b687feadaec52ae1ab3243fd64a9a225cb63fb98544b1b91e",
        "payload": "0x00e3f0f2d4c86c4530df0a9b2b43d2f7e518f204c0af348fb687d23365d794cd0e88aadf48d2bc238cb8dc8fbc0327fb2013450de909c92a9572f557f61e0cb03c2fc5e197ed4f376b329ec945ae2e5e40cb7fe8dc9ebef1b08d21570f8dd3eded591a786b3902af010840f2fa3fabe3f49303faeca8a72ee0941f62d447a700da3a7f2cfe2faa041cc9601f52eb0d12e157145a58234531b3306a43bf3b63bf7b1d2ed0953bdb875d3763d5837cd71307b8cf2b5427cf3a083275f0d14eea61d513a72b16a572fe9a87f7399e5ee8540927370aae0c3188ec746e3144b60da4ccb2a80e78e485d0a54fb5398f54063bed2bb3e017ce4cdc2a0cd3c8edc646d9a239ffd30359be6323e5f4934fecdbcc5742ebc662ff5b1f2086aa2895475a8ec7bb2398ef8c9878fa64d50e725d5fc5bdc3783b8455d25de3a5ef87881790e0a88ef5d75e8724fbed93630edd417696a917cb48bc14f7cd0bdbcee70923e598236bd462f634387e6cf415ae6fc366e93b661738e8622e32e0de05d7da3551696c53776ca941bb8d7b19f8b4920a9435bc68ea12e7d609beb67ef95ddd35feb8e5891fc9869644942fd259b7bb690ec60551026bed930efd919473340f0560997b104cb69e671a211ce4f575af70b96b"
      }
    }
  },
  "ClientHelloInner": {
    "msg_type": "0x01",
    "legacy_version": "0x0303",
    "random": "0xd344d5fdc653ff8f5183ded8f334c64a40f4e5f894088f0e9ffe33bef096cd68",
    "legacy_session_id": "0x1679fc195436817295f55b4bba927826d7f26c0ac6b936d2ea69692a3c00f418",
    "cipher_suites": [
      "0x1302",
      "0x1303",
      "0x1301"
    ],
    "legacy_compression_methods": [
      "0x00"
    ],
    "extensions": {
      "0x0000": {
        "extension_type": "0x0000",
        "server_name": "0x72657365617263682e636c6f7564666c6172652e636f6d"
      },
      "0x002b": {
        "extension_type": "0x002b",
        "msg_type": "0x01",
        "versions": [
          "0x0304"
        ]
      },
      "0x000d": {
        "extension_type": "0x000d",
        "supported_signature_algorithms": [
          "0x0403",
          "0x0503",
          "0x0603",
          "0x0804",
          "0x0805",
          "0x0806",
          "0x0401",
          "0x0501",
          "0x0601"
        ]
      },
      "0x000a": {
        "extension_type": "0x000a",
        "named_group_list": [
          "0x0017",
          "0x0018",
          "0x0019"
        ]
      },
      "0x0033": {
        "extension_type": "0x0033",
        "msg_type": "0x01",
        "key_share_entry": [
          {
            "group": "0x0017",
            "key_exchange": "0x04e08b2ff2039880820a03a9706661ca6351bb1b4f03befb011478d18a84acee6caf99bcdbc15434038ccbeb2eac727dc0e223c173d0ae30510679131de0a28937"
          },
          {
            "group": "0x0018",
            "key_exchange": "0x04646da67d01eb822c3438f73c23cc1f110d327798ed494b0f3e80f0f610da9e23ee7c40ba0416e58b6ae7b07c144aed35d81aead168a24488f028894d2fad3507deb22fdbff1fadf2a52e99e44596315bc43d99d1bd14be334e6de522282e86b3"
          },
          {
            "group": "0x0019",
            "key_exchange": "0x04012e6c12fc453a168987c0188b328c9e3c0da51761b5f7c2dc2a91037ca80771d5b5f434c9070bb3103e966fb0528409152b5ab65c9bb4ac2d21801dd4c70cd88028003a5faef2b03632cd956987ed9e1e9e6cb38a94f5c82a97826788d3a8ba0ce9477f82bcfc56adf2bc57d1abdbbd1602b401465f7e344ed682d30d2f6e3d7aa955c9"
          }
        ]
      },
      "0xfe0d": {
        "extension_type": "0xfe0d",
        "type": "0x01",
        "cipher_suite": null,
        "config_id": null,
        "enc": null,
        "payload": null
      }
    }
  }
}

echspec の実装について

本章では、echspec の実装において特に注目している部分について説明します。
ちなみに、echspec は、HTTP/2 の仕様準拠テストツール h2spec に大変影響を受けました。

HPKE

先ず、ECH は仕様として HPKE (Hybrid Public Key Encryption) という仕組みを用います。

ECH を実装するには、HPKE の実装が必要です。Ruby の HPKE 実装 hpke-rb が公開されています。

これを利用して、TLS 1.3 自前実装である tttls1.3 を ECH に対応させました。

テストケース

echspectttls1.3 をオーバーライドしています。例えば、「メッセージの 0 パディング部分を 0 以外でパディングする」などオーバーライドしています。テストケースごとに、サーバの特定の振る舞いを検証するためにクライアントの動作をオーバーライドして実装しました。

$ tree lib/echspec/spec/
lib/echspec/spec/
├── 5.1-10.rb
├── 5.1-9.rb
├── 7-5.rb
├── 7.1-11.rb
├── 7.1-14.2.1.rb
├── 7.1.1-2.rb
├── 7.1.1-5.rb
└── 9.rb

1 directory, 8 files

パターンマッチによるテスト結果出力

echspec では、TLS ハンドシェイクがアラートメッセージによって中止されても、プログラムを停止させません。なぜなら、テストケースによってはサーバから特定のアラートメッセージを受け取ることを期待するためです。tttls1.3 はアラートメッセージを受け取った際に例外オブジェクトを生成しますが、echspec は各テストケースの結果を EchSpec::Ok または EchSpec::Err クラスで包んで返すようにしています。つまり、テストケースの呼び出し元に例外を返すのではなく、EchSpec::Ok または EchSpec::Err クラスを返すということです。例えば、サーバから仕様通りのアラートメッセージを受け取った場合は EchSpec::Ok を返し、サーバが仕様に反するメッセージを受け取った場合はメッセージスタックなどを EchSpec::Err に包んで返します。呼び出し元は、EchSpec::Ok または EchSpec::Err クラスをパターンマッチしてテスト結果を抽出して出力したり、サマライズしたりします。

下記はテスト結果を出力する部分の実装です。

      def print_summarize(result, desc)
        check = "\u2714"
        cross = "\u0078"
        summary = case result
                  in Ok
                    "\t#{check} #{desc}".green
                  in Err
                    "\t#{cross} #{desc}".red
                  end
        puts summary
      end

おわりに

今後テストケースを増やしていきたいと考えています。
ぜひお試しいただき、フィードバックをいただけると嬉しいです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?