OpenID AXのセキュリティ問題をダラダラとつづる

こんばんは、ritouです。
連休中に、なんかOpenIDとかAXとかなつかしげなネタがふってきたので取り上げます。

いったい何があったのでしょうか。

OpenIDのこと知ってる人向けに簡単にいうと

以下のような場合に、悪い人がレスポンスをいじくって攻撃できちゃうかもねという話ですね。

  • RPがopenid.signedに含まれていないAXパラメータの値を使ってしまう(ようなライブラリを使っている)

シナリオとして、AXで渡されるEmailをユーザー判定に使っているようなRPだとけっこうやばいんじゃないの?みたいなのがありましたが、あまり例は良くないような。
理解できた人はここから先は読んでいただかなくてけっこうですが、時間がある方はダラダラといきましょう。

OpenIDのレスポンスと署名

久々ですが、OpenIDの署名についての仕様を覚えていますか?
OpenIDのPositive Assertionには署名が含まれます。

少し仕様を振り返りましょう。
日本語訳の仕様では、"6.1. 署名の生成手順"のあたりです。

6.1. 署名の生成手順

署名の生成手順は、以下の通りである。

1. 署名すべきメッセージに応じて、署名すべきキーのリストを決定する (Section 10.1 (肯定アサーション) 参照)。署名すべきキーのリストは、メッセージの一部でなければならない (MUST)。リストは "openid.signed" キーとともに格納される。値は、"openid." プレフィックスを削除したコンマ区切りのキーのリストである。このアルゴリズムは、"openid." で始まるキーの署名にのみ用いることができる。
2. "openid.signed" に示された順序で、署名すべきキーのリストそれぞれについて、メッセージ中から "openid." で始まり同じキーを持つ値を探す。
3. 署名すべキーと値のペアのリストを、キー・バリュー形式エンコーディング (キー・バリュー形式エンコーディング)でエンコードしてオクテット文字列に変換する。
4. アソシエーション形式 (アソシエーションの確立)から署名アルゴリズム (署名アルゴリズム)を決定する。その署名アルゴリズムをオクテット文字列に適用する。

Final: OpenID Authentication 2.0 - 最終版

ということで、実は全部の値をごにょごにょして署名を作るのではなく、openid.signedに指定されたパラメータをごにょごにょした署名がopenid.sigとして渡されるということなのです。
Yahoo! JAPANOpenIDのレスポンスに含まれる、openid.signedパラメータを見てみましょう。

&openid.signed=assoc_handle%2C
claimed_id%2C 
identity%2C
mode%2C
ns%2C
op_endpoint%2C
response_nonce%2C
return_to%2C
signed%2C
ns.pape%2C
pape.auth_policies%2C
pape.auth_level.ns.nist%2C
pape.auth_level.nist

たくさんあります。
なんとなくopenid.realmだけ入っていないことに今気付きましたが、まぁ特に問題ではないような。
OPから送られたレスポンスであることを署名から検証できるわけなので、普通はほとんど全部のパラメータが突っ込まれています。

AXだと何がどうなるのか

AXってのは、属性情報を要求したり更新したりできる拡張仕様ですが、署名周りのしくみは上記のとおり変わりません。
良くつかわれるfetchリクエストを使うと、OPからのレスポンスに属性情報が追加されてくるわけです。

Yahoo! JAPANOpenID AXのopenid.signedパラメータを見てみましょう。
Y!JのAX対応についての過去エントリはこちら

openid.signed=assoc_handle%2C
claimed_id%2C
identity%2C
mode%2Cns%2C
op_endpoint%2C
response_nonce%2C
return_to%2C
signed%2C
ax.value.nickname%2C
ax.type.nickname%2C
ax.value.gender%2C
ax.type.gender%2C
ax.value.firstname%2C
ax.type.firstname%2C
ax.value.lastname%2C
ax.type.lastname%2C
ax.value.birthyear%2C
ax.type.birthyear%2C
ax.value.image%2C
ax.type.image%2C
ns.ax%2C
ax.mode%2C
ns.pape%2C
pape.auth_policies%2C
pape.auth_level.ns.nist%2C
pape.auth_level.nist

axから始まるパラメータが入っていることがわかりますね。
このように、OPから渡されたデータを署名から検証できたときのみ、そのAXの値もOPから送られたものだと言えるわけです。

結局何が問題なのか

上の例では、AX関連のパラメータがopenid.signedに含まれていない場合はどのような扱いになるでしょうか?"OPのレスポンスが無効である"とは、言えません。署名に含むかどうかは任意ですからね。
その代わり、ここでは"openid.signedに含まれていないAXの値は信用できないので使ってはいけない"と言えます。

こんなの当たり前だと思いますが、ライブラリに欠陥があったら話は別です。
こういう実装はやばいですよね。

  • openid.から始まるレスポンスパラメータを配列に格納
  • openid.signedに含まれるパラメータから署名を検証し、問題ない場合はレスポンスはOPが作成したものとする
  • OPが作成したレスポンスなんだから全てのopenid.から始まるパラメータはその後の処理に使って良し!

これを使った、頭がおかしいRPと攻撃者の例を見てみましょう。

  • 1. あるRPは、AX対応しているOPにリクエストを送って、AXのEmailを使ってユーザー識別をしている
  • 2. 攻撃者は、AX拡張をつけないリクエストをOPに送り、自分のレスポンスを作成(RPにはまだ渡さない)
  • 3. 攻撃者は、それに他人のEmailを指定した偽のAXパラメータを追加する(openid.signedなどはそのまま)
  • 4. RPは、openid.signedに含まれるAXを含まないレスポンスの署名検証を行った結果、偽のAXパラメータまでも信用する
  • 5. 他人のEmailの持ち主として、攻撃者はログイン状態に・・・

1でclaimed_idとか使わずEmailで識別する時点でおかしいですが。。。
ということで、この機会にRPの皆様は自分のところがおかしな実装になってないか確認してみてはいかがでしょうか?

ではまたー。