QUICプロトコルの解析 - Client Hello/Server Hello
QUICではTLS1.3による暗号化・認証 (AEAD; Authenticated Encryption with Additional Data) が採用されており、InitialメッセージではTLS1.3の暗号化・認証アルゴリズム合意(Client Hello/Server Hello)及び暗号化・認証に必要なKey情報(key_share)の交換が行われる。
TLS1.2までとは異なり、Client HelloにExtensionとしてkey_shareを設定し、Client Helloでのクライアントからのアルゴリズム提案と同時に必要なkey情報をServerに渡すことで、TLSハンドシェークの短縮を実現している。
今回はClient Hello / Server Helloシーケンス
QUICのClient Hello/Server Helloシーケンス
今回は、前回のInitialシーケンスの記事で紹介したシーケンスのうち、Initialシーケンスと同時に実施されるClient Helloシーケンスと、Initialシーケンスの後に実施されるServer Helloシーケンスについて。
Client Hello / Server Helloシーケンス
Client Hello / Server Helloシーケンスでは、Cryptoフレームを利用して暗号化アルゴリズムのネゴシエーションを実施する。
Client Hello
TLSのプロトコルバージョン、暗号化に用いるRandom Nonce、使用する暗号化スイートの候補、拡張(Extension)をサーバに通知する。
プロトコルバージョン
TLS1.3の場合、プロトコルバージョンは1.2(0x0303)を通知し、Extension ID=43 (Supported Version) でTLS1.3 (0x0304)を通知する。
暗号化スイート候補
暗号化スイート候補は以下のように複数のスイートを並べることができる。
Cipher Suites (3 suites) Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302) Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
上記の例では、以下のようなスイートが候補となっている。
- 認証付きAES128 GCMモード (ガロアカウンタモード) + SHA256ハッシュ
- 認証付きAES256 GCMモード (ガロアカウンタモード) + SHA384ハッシュ
- CHACHA20 POLY1305 + SHA256ハッシュ
拡張 (Extension)
ExtensionではRFC 8446 4.2章で定義されている拡張を含めることができる。代表的なものは以下の通り。 また、RFC 9001 8.2章で定義されるQUIC Transport Parametersを含める必要がある。
- Server Name (ID=0)
- Supported Groups (ID=10)
- ALPN; Application Layer Protocol Negotiation (ID=16)
- Key Share (ID=51)
- QUIC Transport Parameters (ID=65445 (draft 13))
Key Shareの例
Extension: key_share (len=38) Type: key_share (51) Length: 38 Key Share extension Client Key Share Length: 36 Key Share Entry: Group: x25519, Key Exchange length: 32 Group: x25519 (29) Key Exchange Length: 32 Key Exchange: 5f3994e8d5ea48bc37bde49a8311046d105b92782673022e3d7a145e00908d78
Server Hello
Client Helloを受け、サーバ側が受け入れるバージョンとKey ShareをPacket Type = InitialのCRYPTOフレームで送信する。 Server Helloを送信するパケットではPacket Type = HandshakeのCRYPTOフレームも連結して送信するが、Handshakeフレームについては後述。
以下の例では、Client Helloでクライアントから提案されたTLS 1.3で鍵交換はECDHE X25519に対して、同じ値でServer Helloが応答していることからサーバはクライアント提案のTLS 1.3 / ECDHE X25519を受け入れることを示している。
Key Exchangeでサーバが応答する値は、サーバ側の公開鍵QBである。
サーバ側はQAと楕円曲線X25519で定義される有限体K、曲線を決定する3次式、ベースポイントG、その位数nなどのパラメータに従ってクライアント側の秘密鍵dBを計算する。 クライアント側はQBと楕円曲線X25519で定義されるパラメータに従ってサーバ側の秘密鍵dAを計算する。
Extension: key_share (len=38) Type: key_share (51) Length: 38 Key Share extension Client Key Share Length: 36 Key Share Entry: Group: x25519, Key Exchange length: 32 Group: x25519 (29) Key Exchange Length: 32 Key Exchange: 5f3994e8d5ea48bc37bde49a8311046d105b92782673022e3d7a145e00908d78 Extension: supported_versions (len=2) Type: supported_versions (43) Length: 2 Supported Version: TLS 1.3 (0x0304)
QUICプロトコルの解析 - Initialシーケンス
前回の続き。
- QUICの1-RTT Handshakeシーケンス
- Initialシーケンス
- パケットフォーマット
- Initialパケット
- Header Form (1bit)
- Fixed Bit (1bit)
- Long Packet Type (2bits)
- Reserved Bit (2bits)
- Packet Number Length (2bits)
- Version (32bits)
- Destination Connection ID Length (8bits)
- Destination Connection ID (0~160bits)
- Source Connection ID Length (8bits)
- Source Connection ID (0~160bits)
- Token Length (8bits)
- Token (~256Bytes)
- Packet Length (1~8Bytes)
- Packet Number (1~4Bytes)
- Retryパケット
- Initialパケット
QUICの1-RTT Handshakeシーケンス
QUICの初期セッションの確立においては、1-RTT Handshakeシーケンスを用いる。 Initial及びRetryシーケンス、Handshakeシーケンスから構成される。
今回はInitialシーケンスについて。
Initialシーケンス
InitialシーケンスではServer - Client間でConnection IDの設定を実施する。 (同時に実施されるClient Hello/Server HelloからHandshakeシーケンスについては次回)
Initial(Client → Server 1回目)
ClientからServerに対してDestination Connection ID (サーバを識別するコネクションID) を通知する。
ClientがServerからInitialパケット or Retryパケットを受信していない場合、Clientは8Bytes以上のランダム値をDestination Connection IDフィールドに設定する(MUST)。 [RFC 9000 7.2]
続くペイロードにはTLS 1.3のClient Helloを含める。
以下の例だとa39456398973fb79
が該当。
QUIC IETF 1... .... = Header Form: Long Header (1) .1.. .... = Fixed Bit: True ..00 .... = Packet Type: Initial (0) .... 00.. = Reserved: 0 .... ..00 = Packet Number Length: 1 bytes (0) Version: draft-29 (0xff00001d) Destination Connection ID Length: 8 Destination Connection ID: a39456398973fb79 Source Connection ID Length: 0 Token Length: 0 Length: 1332 Packet Number: 3
Retry (Server → Client)
Serverは、Server側のコネクションを識別するためのDestination Connection IDを任意に設定し、RetryパケットのSource Connection IDフィールドに設定してClientに送信する。
続くInitialを識別するためのRetry Tokenと、Retryパケットの正当性確認のためのRetry Integrity Tag [RFC 9001 5.8] も付与する。
以下の例だと459be263f78e84e90f99f4e66af27b2690e9c583
がコネクションID。
QUIC IETF 1... .... = Header Form: Long Header (1) ..11 .... = Packet Type: Retry (3) Version: draft-29 (0xff00001d) Destination Connection ID Length: 0 Source Connection ID Length: 20 Source Connection ID: 459be263f78e84e90f99f4e66af27b2690e9c583 Retry Token: 576af22978557f29143112e3ec2af60b9834b7abf3390281592b107978d6aaaa622e4927… Retry Integrity Tag: 6149bd84c4c658e28937f499268ef74f
Initial(Client → Server 2回目)
Retryパケットの受信後、ClientはRetryパケットに含まれるSource Connection IDでDestination Connection IDを更新する。[RFC 9000 7.2]
また、Retryパケット受信を契機としたConnection ID変更であることの正当性担保のため、Retryパケットに含まれるRetry TokenをTokenフィールドに設定して送信する。[RFC 9000 17.2.5.2]
続くペイロードに含めるTLS 1.3のClient Helloは、1回目のInitialと同じものを設定する。
QUIC IETF QUIC Connection information [Packet Length: 1350] 1... .... = Header Form: Long Header (1) .1.. .... = Fixed Bit: True ..00 .... = Packet Type: Initial (0) .... 00.. = Reserved: 0 .... ..00 = Packet Number Length: 1 bytes (0) Version: draft-29 (0xff00001d) Destination Connection ID Length: 20 Destination Connection ID: 459be263f78e84e90f99f4e66af27b2690e9c583 Source Connection ID Length: 0 Token Length: 64 Token: 576af22978557f29143112e3ec2af60b9834b7abf3390281592b107978d6aaaa622e4927… Length: 1255 Packet Number: 4
Initial (Server → Client)
Clientからの2回目のInitialが問題ない場合、ServerはInitialパケットで応答する。 ServerはClientを識別するためのSource Connection IDを設定する。
続くペイロードにはTLS 1.3のServer Hello/ACKを含めて送信する。さらに、Initialパケットに続けてHandshakeパケットを同一UDPデータグラムに設定する。
QUIC IETF 1... .... = Header Form: Long Header (1) .1.. .... = Fixed Bit: True ..00 .... = Packet Type: Initial (0) .... 00.. = Reserved: 0 .... ..00 = Packet Number Length: 1 bytes (0) Version: draft-29 (0xff00001d) Destination Connection ID Length: 0 Source Connection ID Length: 20 Source Connection ID: bdfa6963f4b15db20457c3262371f4c85764ba7d Token Length: 0 Length: 116 Packet Number: 0 Payload: 1afd2a2ef815351a8cf07bb40341aafad55a798b09a1eff9e5c8ea77135b6b49bd90091c… TLSv1.3 Record Layer: Handshake Protocol: Server Hello ACK QUIC IETF 1... .... = Header Form: Long Header (1) .1.. .... = Fixed Bit: True ..10 .... = Packet Type: Handshake (2) .... 00.. = Reserved: 0 .... ..00 = Packet Number Length: 1 bytes (0) Version: draft-29 (0xff00001d) Destination Connection ID Length: 0 Source Connection ID Length: 20 Source Connection ID: bdfa6963f4b15db20457c3262371f4c85764ba7d Length: 1071 Packet Number: 0 Payload: 077be1a4fe43c6564ef3f20bba747a1cbf3e3b65209252d2c8414b6bb151694589df8b8a… TLSv1.3 Record Layer: Handshake Protocol: Multiple Handshake Messages
パケットフォーマット
Initialパケット
RFC 9000 17.2.2章で以下のように定義されている。
Initial Packet { Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2) = 0, Reserved Bits (2), Packet Number Length (2), Version (32), Destination Connection ID Length (8), Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160), Token Length (i), Token (..), Length (i), Packet Number (8..32), Packet Payload (8..), }
Header Form (1bit)
- Long Header Packetでは1、Short Header Packetでは0を設定する。
- Initial PacketはLong Header Packetのみなので "1" 固定。
Fixed Bit (1bit)
- Version Negotiation Packet以外では1を設定する。
- Initial Packetでは "1" 固定。
Long Packet Type (2bits)
- Initial Packetでは "00" を設定する。
- 他のパケットタイプの設定値は以下の通り
- 0-RTT Packet : 01
- Handshake Packet : 02
- Retry Packet : 03
Reserved Bit (2bits)
- "00"を設定する。
Packet Number Length (2bits)
- Packet Numberフィールド長(Bytes) - 1を設定する (e.g., 1 Bytesなら0を設定)
Version (32bits)
- 準拠するQUICのバージョンを設定する。
- draft-29の場合、
0xff00001d
を設定。- 上位2バイト : draftの場合、ffに設定
- 下位6バイト : バージョンを設定 (29 = 0x1d)
Destination Connection ID Length (8bits)
- Destination Connection ID長(Bytes)を設定する。
- 最大20Bytesなので最大値は20。
Destination Connection ID (0~160bits)
- Server側でコネクションを識別するためのDestination Connection IDを設定する。
- Initial送信時以降、ServerからのRetry受信時、ServerからのInitial受信時の2回更新される。
Source Connection ID Length (8bits)
- Source Connection ID長(Bytes)を設定する。
- 最大20Bytesなので最大値は20。
Source Connection ID (0~160bits)
- Client側でコネクションを識別するためのSource Connection IDを設定する。
Token Length (8bits)
- 次に続くToken長(Bytes)を設定する。
- Tokenが存在しない場合は"0"を設定する。
- ServerはInitialにTokenを設定しない。
Token (~256Bytes)
- Client verificationのため、RetryパケットでRetry Tokenが指定された場合にはRetry Tokenと同値を入れてInitialを再送する。
Packet Length (1~8Bytes)
- Packet Lengthより後のパケット長を示す。
- = Packet NumberフィールドとPacket Payloadフィールド長の合計。
- 最上位2ビットの値によってPacket Lengthに使用するビット長が変化する (Section 16)。
- 最上位2ビットが"01"の時、Packet Lengthは14bits (0-16383)となる。
- e.g., 0x4534 = 0100 0101 0011 0100 → 上位2ビットが"01"のため、続く14ビットがPacket Length。
- 続く14ビットは00 0101 0011 0100 = 0x534 = 1332なのでPacket Length = 1332Bytes。
Packet Number (1~4Bytes)
- Packet Number Lengthフィールドで指定された長さのフィールドで、パケット番号を設定する。
Retryパケット
RFC 9000 17.2.5章で以下のように定義されている。
Retry Packet { Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2) = 3, Unused (4), Version (32), Destination Connection ID Length (8), Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160), Retry Token (..), Retry Integrity Tag (128), }
Header Form (1bit)
- Long Header Packetでは1、Short Header Packetでは0を設定する。
- Retry PacketはLong Header Packetのみなので "1" 固定。
Fixed Bit (1bit)
- Version Negotiation Packet以外では1を設定する。
- Retry Packetでは "1" 固定。
Long Packet Type (2bits)
- Retry Packetでは "0x03" を設定する。
- 他のパケットタイプの設定値は以下の通り
- 0-RTT Packet : 0x01
- Handshake Packet : 0x02
- Retry Packet : 0x03
Version (32bits)
- 準拠するQUICのバージョンを設定する。
- draft-29の場合、
0xff00001d
を設定。- 上位2バイト : draftの場合、ffに設定
- 下位6バイト : バージョンを設定 (29 = 0x1d)
Destination Connection ID Length (8bits)
- Destination Connection ID長(Bytes)を設定する。
- Retry PacketではDestination Connection IDは設定しないので0を設定。
- 最大20Bytesなので最大値は20。
Destination Connection ID (0~160bits)
- Server側でコネクションを識別するためのDestination Connection IDを設定する。
- Retry PacketではDestination Connection IDは設定しない。
Source Connection ID Length (8bits)
- Source Connection ID長(Bytes)を設定する。
- 最大20Bytesなので最大値は20。
Source Connection ID (0~160bits)
- Client側で2回目のInitialで設定すべきServer側のDestination Connection IDを設定する。
- ClientはRetry Packetで受信したSource Connection IDを次回のInitial packetのDestination Connection IDに設定する。
Token Length (8bits)
- 次に続くToken長(Bytes)を設定する。
- Retry Packetでは設定必須。
Token (~256Bytes)
- Server側でRetryに伴うInitialを識別するため、Initial再送時にClientに設定してもらうRetry Tokenを指定する。
Retry Integrity Tag (128bits)
- RFC 9001 5.8で規定される、Retryパケットの完全性確認 (Integrity) のためのタグ
- AEAD_AES_128_GCMの認証付き暗号 (AEAD: Authenticated Encryption with Associated Data) を利用して認証と完全性の確認を行う
長くなってきたので、Handshakeシーケンスに続きます…
QUICプロトコルの解析 - パケットキャプチャ
はじめに
QUICプロトコルは2021年5月にRFC 9000で標準化されたプロトコル。HTTP/3と併せて使うことが想定されている。
UDPベースのトランスポートを提供しており、データに加えてヘッダも暗号化されて伝送されるため、従来のTCPのようなシーケンス番号による解析が難しくなっている。
QUICのパケットキャプチャ
nginxが用意しているQUICのテスト環境を使ってパケットキャプチャしてみる。 テストの環境は以下の通り
- macOS Big Sur 11.5
- Google Chrome 92.0.4515.107
- Wireshark 3.4.6
Google ChromeをTLS pre-master-secret取得モードで起動
QUICはTLSで暗号化がかかるため、Google ChromeをTLSのpre-master-secretを取得できるようにして起動する。
SSLKEYLOGFIKE=
でファイル名を指定して起動させることで、指定したファイルにTLS復号のためのpre-master-secretが保存される。
% SSLKEYLOGFILE=~/Capture/pre-master-secret.log "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
ChromeのQUICサポートを明示的に有効にする
起動したChromeでchrome://flags
にアクセスし、実験的なQUICプロトコルのサポートと、DNSでのHTTPSSVCレコードのサポートを有効にしておく。
nginxのQUICテスト環境へのアクセス
Wiresharkでパケットキャプチャを開始したのち、nginxが用意しているQUICのテスト環境にアクセスし、TestをRunする。 QUICでアクセスできたセッションは緑色で表示される。
WiresharkでのQUICの見え方
InitialとHandshakeの1パケット目まではTLS1.3のネゴシエーション前のため、ヘッダが平文で見える。
Handshakeの2パケット目以降はTLS1.3のネゴシエーションが完了するため、ヘッダ以降が全てTLS暗号化される。
WiresharkでのTLSのデコード
Preference->Protocols->TLSからSSLKEYLOGFILEで指定したpre-master-secretのファイルを指定する。
pre-master-secretを指定することで、Packet NumberやPayloadなどを復号してみることができるようになる。
おわりに
今回はQUICパケットのキャプチャとTLSの復号までを試してみました。
MacBookを利用したWi-Fiパケットのキャプチャ
Wi-Fiの 自分自身以外の パケットをキャプチャして解析してみます。
macOS 10.15 CatalinaまではWireshark単体で実施できましたが、macOS 11.0.1 Big SurからはWiresharkからは実施できない模様。
接続中の周波数・帯域幅の確認
Altキーを押しながらWi-Fiアイコンをクリックし、接続中のアクセスポイントの周波数・帯域幅を確認する。
ワイヤレス診断の起動とキャプチャの開始
以下のフォルダにある "ワイヤレス診断" を起動する。Spotlightからも検索できる。
/System/Library/CoreServices/Applications/
Windowメニューから "Sniffer" を選んで起動する。
チャンネルと帯域幅を指定してStartをクリック。ここで指定したチャンネルと帯域幅で通信している端末について、自身の端末以外のパケットも全てキャプチャする。
端末のアクセスポイントへの接続
キャプチャ開始以降に当該アクセスポイントに接続した端末についてはパケット復号が可能。そのため、配下端末から当該APに再接続させる。
再接続後、適当なWebサイトなどを閲覧しておく。
キャプチャの停止
Sniffer画面から "Stop" をクリックしてキャプチャを停止させる。
キャプチャデータの確認
キャプチャデータは /var/tmp/
に格納される。⌘Gなどでフォルダ移動して表示させる。
Wiresharkで開くとキャプチャできていることがわかる。暗号化解除などの方法は次回。
Wireshark画面のカスタマイズ
Wiresharkの画面をカスタマイズしてみます。
Preference(設定)メニューの表示
macOSなら
Wireshark > Preferences...
で設定画面を表示させます。
Windowsなら
編集 > 設定
で表示。
ペインレイアウトのカスタマイズ
設定画面が表示されたら、
Appearance > Layout
でペインレイアウトの設定画面を表示させます。
レイアウト設定
設定画面の上段からレイアウトを選択します。 お好みで選択すればOK。
表示項目設定
各ペインに何を表示させるかを設定します。
- Packet List
- パケット一覧を表示させる
- Packet Details
- パケットをデコードした結果を表示させる
- Packet Bytes
- パケットのバイト列を表示させる(ビット表示も可能)
- Packet Diagram
- パケット構造のイメージを表示させる
- None
- 何も表示させない
パケット一覧の表示列のカスタマイズ
パケット一覧に表示させる列をカスタマイズしてみます。
Appearance > Columns
で列の設定画面を表示させます。
列の追加
"+"マークを押下して新規列を追加してください。
今回は例として「絶対時刻をYYY-MM-DD hh:mm:ss.000形式で表示させる」列を追加してみます。 列名は"New Column"と出てくるものをダブルクリックして適宜名称変更します (例えば"Date"など)。表示内容はType部分をダブルクリックすると設定可能な項目が出てくるので、"Absolute date, as YYY-MM-DD, and time"を選択します。
以上で設定は完了です。 ドラッグすると表示順を並べ替えられます。
Wiresharkでパケットキャプチャをやってみる
Wiresharkを使って実際にパケットキャプチャをしてみます。
パケットキャプチャの開始と停止
キャプチャ対象のインターフェースを選択します。
ここでは Wi-Fi: en0
をサンプルとして選択。
対象のPCで通信に使っているインターフェースを選択してください。
選択後、左上のサメのヒレのようなマークをクリックするとパケットキャプチャが開始されます。 パケットキャプチャを開始した状態で、ブラウザ等で適当なページを表示させると、キャプチャされたパケットデータがWiresharkに表示されていきます。
停止したい時はヒレのマークの隣の赤い四角マークをクリックすると停止します。
キャプチャ結果の表示
パケットキャプチャを開始後、キャプチャしているPCが送受信したパケットが表示されます。
- パケットリスト
- パケット詳細
- パケットバイト
が表示されます。 (画像は表示レイアウトをカスタマイズしています)
パケットリストペイン
キャプチャしたパケットの一覧が表示されます。
一覧に表示されているパケットをクリックすると、パケット詳細ペインに対象パケットのプロトコルツリーが表示されます。
パケット詳細ペイン
パケットリストペインで選択されているパケットの詳細情報 (プロトコルツリー) が表示されます。
TCP/IPの各レイヤプロトコルで何を使用していて、どのような情報が書き込まれているかなど。実際の解析では、このパケット詳細ペインに表示されている情報を使いながら解析を進めていきます。
パケットバイトペイン
パケットリストペインで選択されているパケットのバイト列が表示されます。
解析で参照することはあまりありませんが、直接バイト列を確認したい場合などに使います。
パケットキャプチャの保存
パケットキャプチャを停止した状態ではファイルとして保存されていないため、あとで解析する場合はキャプチャデータの保存を行います。
macOSでは "File → Save" で保存できます。 適当な名前をつけて "Save" で保存します。
Wiresharkのインストール
まずはWiresharkをインストールしてみます。
Wiresharkのダウンロード
以下のサイトからご自身のOSに合わせたパッケージをダウンロードします。
Wiresharkのインストール
macOS
ディスクイメージの展開とインストール
ダウンロードしたdmgをダブルクリックして開くとディスクイメージが展開されるので、"Wireshark"をアプリケーションフォルダにドラッグ&ドロップすればWireshark本体のインストールは完了する。
パケットキャプチャに必要な権限変更
"Install ChmodBPF"をダブルクリックしてインストールする。本パッケージはパケットキャプチャに必要な権限変更を実施してくれる。
システムパスへの追加
"Add Wireshark to the system path"をダブルクリックしてインストールする。本パッケージはWiresharkをシステムパスに追加することで、コマンドライン(ターミナル)からの操作を可能にする。
Wiresharkの起動
アプリケーションフォルダ内にWiresharkがコピーされているので、ダブルクリックして起動する。初回起動時は警告画面が出るが、Openをクリックして起動を継続する。
起動完了後の画面。BPFの権限変更ができていない場合は警告が表示される。パケットキャプチャを実施しない場合は無視してもOK。
パスの確認
コマンドラインからの操作を実施したい場合、ターミナルを起動しパスを確認しておく。/Applications/Wireshark.app/Contents/MacOS/
が含まれていればOK。
~ % echo $PATH /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/Wireshark.app/Contents/MacOS/
Linux
使用しているディストリビューションのパッケージ管理システムに合わせてインストールする。
Ubuntu系
$ sudo apt-get install wireshark
CentOS(RHEL)系
$ sudo yum install wireshark
Windows
インストーラーをダブルクリックしてインストールする。
アップデート予定