これは、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 に対応させました。
テストケース
echspec
は tttls1.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
おわりに
今後テストケースを増やしていきたいと考えています。
ぜひお試しいただき、フィードバックをいただけると嬉しいです。