WebSocketのプロトコル
世間から周回遅れですが事情によりWebSocketのことを調べてみました。
WebSocketの一次情報のリンクを挙げておきます。
WebSocketプロトコル
WebSocket API
まだプロトコルとして枯れていません。プロトコル仕様の日本語訳もありますが情報が古いので最新版と互換性がありません。この文書は2010年5月23日版の仕様を元にしています。情報がいつまで有効かは不明なので読むときは気をつけてください。
昔、なんでもHTTPという風潮が嫌いでした。通信プロトコルは適材適所であるべきだと思ったからです。HTTPに向いていない用途にまで無理矢理HTTPでラップするのが嫌いでした。いつしか、まあいいかHTTPで、と思うようになりました。文字コードはUTF8でいいか、と思い始めた時と同じ時期かもしれません。多様性は善というお題目を素直に信じられなくなった頃です。
とは言え、HTTPでプッシュ技術をするためにlong pollingをするのはいかがなものか、と思う程度の良識は持ち合わせています。別にlong pollingを全否定したいわけではありません。ハックとしてはありです。しかし、あくまでハックであって、表舞台で堂々と使う技法ではありません。
概要
- クライアントからサーバにTCP接続してhandshakeする
- handshakeのリクエスト/レスポンスのやりとりはHTTPそのもの
- クライアントはリクエストボディで8バイトのランダム文字列(challenge)を送る
- サーバは16バイトのresponse文字列を返す
- handshake後、TCP接続は張りっぱなしにする
- handshake後のTCP接続上、双方向にデータを送信可能
- テキストデータは 0x00 で始まり 0xff で終了するフレーム単位
- テキストデータの文字コードはUTF8
- バイナリデータも送れる(タイプと長さを最初に送る)
- URLは ws:// or wss://
- 紳士的な切断は 0xff 0x00 を送って、0xff 0x00 を受信してからTCPレベルで切断
考察
TCP上の通信プロトコルの定石はTLV(type,length,value)の三つ組によるデータ送信です。
最初に型情報、次にデータ長、最後にデータを送ります。TCP接続を張ったままデータをだらだらと送り続けるプロトコルではこのスタイルが一般的です。データ長を最初に送ってその長さ分のデータを送るスタイルは受信側に優しいプロトコルです。最初にどのぐらいのデータを読めばいいかわかるのでプログラミング(特にメモリ確保)が簡単になります。読み取りの終端処理もデータの中身をパースすることなく行えます。ただし、送信側は、データの全長がわかるまで送信を開始できない欠点があります。ちなみに、HTTP1.1では、全長がわかる前にデータ送信したい要求のためにChunked Transfer Codingを使えます。
WebSocketの一般的なデータ送信は0x00で始まり0xffで終端するUTF8文字列です。一見、TLVっぽくありません。ただ、プロトコル上はTLVです。0x00はUTF8(0xff終端)のデータ型(frame type)を示すバイトという扱いです。先頭バイトのhigh-orderビットが立っている場合、後続に長さ情報が続くframe typeを意味します。high-orderビットが立っているバイトが続く限り、それらはデータ長を示します。データ長を示すバイト(high-orderビットを除く7ビットずつ)の終端はhigh-orderビットの立たないバイトです。データ長の後にはデータが続きます。受信側はデータ長だけバイト列を読み込みます。プロトコル上、ただのバイト列なのでバイナリデータも送れます。
通常用途では0x00(frame type)でUTF8文字列を送ることが多いと思います。文字列のフォーマットはJSONが良いでしょう。JSONであれば型情報も含めて構造データを送受信できるからです。
handshakeの具体例
GET /demo HTTP/1.1 Host: example.com Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8 Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0 Sec-WebSocket-Protocol: sample ...アプリが使うことを想定したヘッダ Origin: http://example.com Tm[K T2u HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Origin: http://example.com Sec-WebSocket-Location: ws://example.com/demo Sec-WebSocket-Protocol: sample fQJ,fN/4F4!~K~MH
不思議なchallenge-response処理
handshakeリクエストヘッダの例
Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8 Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0
ヘッダ値から数字部分のみ取り出し
1868545188 173347027
ヘッダ値の空白文字を数える
12 10
除算
155712099 173347027
16バイトデータ生成
ふたつの数値を32bit big-endianにエンコーディング
09 47 FA 63 0a 55 10 d3
リクエストボディの8バイトと連結して16バイト生成
Tm[K T2u => 54 6d 5b 4b 20 54 32 75
# 16バイトを書き込んだファイルのmd5値 $ md5sum /tmp/dat 66514a2c664e2f344634217e4b7e4d48 /tmp/dat
16バイトデータのMD5をresponseのボディで送る
fQJ,fN/4F4!~K~MH
# 確認 $ echo 'fQJ,fN/4F4!~K~MH'|od -t x1 0000000 66 51 4a 2c 66 4e 2f 34 46 34 21 7e 4b 7e 4d 48
これは一体何なんでしょうか?
普通のchallenge-responseは共有する秘密情報(普通はユーザのパスワード)を使って認証をします。WebSocketのchallenge-responseにそんなものはありません。処理はムダに複雑です(空白の数を数えるとか)。アルゴリズム的に計算量が爆発するという意味での複雑さには意味がありますが、単に計算が面倒という意味の複雑さはセキュリティ的な強度には無関係です。手作業の計算がいくら面倒だろうと、計算するコードを書いてしまえばおしまいだからです。
結局、これは何を守っているのでしょう。
- Category(s)
- カテゴリなし
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/inoue/websocket/tbping
- 高校生
- ¦
- Main
- ¦
- blogのコメントスパムが多すぎる
Re:WebSocketのプロトコル
参照なさった版は draft-ietf-hybi-thewebsocketprotocol-00 ですかね。
http://www.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-00.txt
これは以前の WebSocket プロトコルから handshake 周りの互換性を壊したあとのバージョンです。
互換性を壊した時の Google 側のアナウンスは以下で、ここでは
"introduces nonce-based challenge-response to protect from cross protocol attacks"
と軽く変更理由に触れています。
http://blog.chromium.org/2010/06/websocket-protocol-updated.html
さて、この文書は 1.3. Opening handshake をご覧になりながら書かれたのだと思いますが、
そこには "This prevents an attacker from tricking a WebSocket server by sending it
carefully-crafted packets using |XMLHttpRequest| or a |form| submission."
とめんどくさい仕組みを入れた理由が書かれてあります。
また、スペースを入れたり2つに分けているのもそれぞれ理由があり、これまたきちんと理由が書かれています。
例えばスペースについては "_dividing_ by this number of spaces is intended to
make sure that even the most naive of implementations will check for
spaces, since if ther server does not verify that there are some
spaces, the server will try to divide by zero, which is usually fatal
(a correct handshake will always have at least one space).
つまるところ、きちんと仕様を読まない人でも安全に実装できるように考慮されているという話ですね。
前後を読むと具体的に想定しているのが HTTP ヘッダの GET/POST 行の PATH 部分からの攻撃であり、
PATH に含むことのできない文字であるスペースを意味のある文字列として入れることで回避しています。
まぁ、この仕様だけから読み取るのは難しい気もするので、MLでの議論も参照します。
HTML5関連の仕様はwhatwgやw3cやIETFなどで作っており、議論も公開されています。
で、WebSocketの場合はIETFで議論を行っています。
https://www.ietf.org/mailman/listinfo/hybi
この handshake 周りの変更に関する議論は、
http://www.ietf.org/mail-archive/web/hybi/current/msg01268.html のスレッドから始まり、
http://www.ietf.org/mail-archive/web/hybi/current/msg01309.html にて
現在のスペースを用いた手法の紹介とその理由が説明されています。
簡単にいうと、適当に正規表現で抜き出して来る実装を不可能にすることによって、
そういった手抜き実装に対してXMLHTTPRequestやformから攻撃できないようにするということです。
割り算にしているのはクライアント側が手抜きをしてスペースを入れないようにすると、
ゼロ除算エラーがでて動かないようにするためとか、紹介した以外の部分では既存のHTTPクライアント/サーバーに
送った場合の互換性を考慮していたりだとか、まぁ細かい所まで気を使っていて泣けます。
HTML5は以上の2つのメールの送信者であるIan Hicksonが主導しているんですが、
この手の気遣いが随所に溢れていて、これこそが当初は中二病プロジェクトだと思われていたHTML5が、
お、なかなかやるじゃないか、という昨今の評価につながっているんだろうなぁと思ったりします。
というわけで、今回のWebSocketのhandshake部分以外でも、何かよりよいアイディアや、
既存実装との互換性において不都合のある点、セキュリティ上の懸念等があれば、
それぞれのMLにフィードバックを送ってあげるととてもよいことでしょう。
Re:WebSocketのプロトコル
とは言え、実は、"This prevents an attacker from tricking a WebSocket server by sending it carefully-crafted packets using |XMLHttpRequest| or a |form| submission."と見ても、何から何を守ろうとしているかわからない、が率直な感想です。
教えてくださいと言うのも厚かましいので、参照先の情報を見てみます。