daily dayflower

2012-07-11

Rack::Auth::Digest::MD5 のつかいかた

Rack::Auth::Digest::MD5opaque を渡さないといけない*1ので素直に書けないと思いがちだけど,現在の Rack::Auth::Digest::MD5 は第2引数opaque をとるので,シンプルに use を使って書ける。

require 'rack/auth/digest/md5'

use Rack::Auth::Digest::MD5, 'my realm', '', do |username|
  'password'
end

run my_app

Padrino の場合はこんなふうに。

Padrino.before_load do
  require 'rack/auth/digest/md5'
  Padrino.use Rack::Auth::Digest::MD5, 'my realm', '', do |username|
    'password'
  end
end

アプリケーションのステートに応じて opaque を返したい場合*2は,結局 Rack::Auth::Digest::MD5インスタンスを生成して rack mount していくしかない気がする。そもそもそんなシチュエーションでは rack middleware じゃ単純には無理かな?


ともかく。

実は名前付きパラメータでも引数を渡せるので下記のようにも書ける。

require 'rack/auth/digest/md5'

use Rack::Auth::Digest::MD5,
  { 
    :realm  => 'the realm',
    :opaque => '',
  },
  do |username|
    'password'
  end

run my_app

ところで。

せっかく HTTP Digest 認証なのに,生パスワードを書かないといけないなんてダッサいと思いませんか。

:passwords_hashed パラメータtrue にすると,ハッシュ化したパスワード等 (いわゆる A1) を渡せばよくなるのでシステムに生パスワードを保存しておく必要がなくなる*3

require 'rack/auth/digest/md5'

REALM  = 'the realm'

require 'digest/md5'
PWHASH = Digest::MD5.new.update('%s:%s:%s' % ['dayflower', REALM, 'password'])

use Rack::Auth::Digest::MD5,
  { 
    :realm            => REALM,
    :opaque           => '',
    :passwords_hashed => true,
  },
  do |username|
    PWHASH
  end

run my_app

PWHASH の算出のところに生パスワード書いてあるけどこれはあくまでサンプルだからであって,あらかじめ計算しておくなり,htdigest コマンドで生成した値を利用するなり,データベースに保存しておくなり,しておけば生パスワードを保存しておく必要はなくなる。

Rack::Auth::Digest::MD5 での nonce のとりあつかい (とバグ)

一般に,HTTP Digest 認証でリプレイ攻撃を「厳密に」防ぐには

  • nonceサーバサイドで生成しサーバに保持しておく
  • クライアントから返された noncenc の組がすでに認証済ならハネる (新しい nonce を生成し,stale を立ててレスポンスするだけでいい)
  • クライアントから返された nonceサーバサイドに保持したものと違えばハネる (上記と同様 stale token とする)

などする必要がある。

Rack::Auth::Digest::MD5 では nonceタイムスタンプになっている。のでサーバサイドに nonce を保持しておく必要がなくなり実装が楽である。

「一定時間」を超えた nonce を破棄していけば,その一定時間を超えた段階でのリプレイ攻撃が成立しなくなるのでカジュアルには,悪い選択肢ではない*4

Rack::Auth::Digest::MD5 ではデフォルトではこの「一定時間」が設定されていない。このことは nonce が破棄されないことをしめしている。なのでリプレイ攻撃やり放題である (サーバサイドに何も保持していないので nc のチェックもしてないし)。

「一定時間」を設定するには Rack::Auth::Digest::Nonce::time_limit に値を設定する。

require 'rack/auth/digest/nonce'

Rack::Auth::Digest::Nonce::time_limit = 10

単位は秒なので,この例でいくとサーバnonce を発行してから10秒経つと無効な nonce となる。時間切れになった場合は,stale 属性の立った WWW-Authenticateサーバが返すので,ユーザーエージェント側でパスワード入力ダイアログが再び出ることはない。エージェントが新しい nonce をもとに自動的に認証ダイジェストを計算して再送信することになる*5

と,これでいいはずなんだけど,現在のところ Rack::Auth::Digest::Nonceバグがあるので,このようにすると常に stale となり延々と認証リクエストレスポンスネゴシエーションが走ってしまう。このバグに対処するには下記のようにすればよい。

require 'rack/auth/digest/nonce'

class Rack::Auth::Digest::Nonce
  def stale?
    !self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit
  end
end

誰も使っていないフィーチャーなのだろう。そもそも安全でない経路で Digest 認証をやることがメジャーではないのかもしれない。

暇ができたら issue 立てて pull request するつもりだけど,テストまで込みで考えるとめんどいなぁ。pull request だしといた。master にマージされた

*1:つうかそもそも opaque は optional なはずなのに Rack::Auth::Digest::MD5 では必須パラメータってのも変なんだけど。

*2:これが本来の opaque の使い方。とはいえ,いろいろ代替手段があるので (nonce に入れ込んじゃうとか Cookie 使うとか) opaque が真面目に使われるケースはないんじゃないかな。

*3:実はハッシュ化された A1 (と username の組) を盗まれると認証できてしまう。なのでハッシュ化して保存したとしてもユーザーの (他のサービスと共用しているかもしれない) 生パスワードが盗まれないという意義しかない。

*4:とはいえ当然ながら「一定時間」内でのリプレイ攻撃は成立してしまうので,攻撃者が盗聴可能な経路でこのような実装を利用するのはやめたほうがよい。

*5:なので time_limit をかなり小さくしてもまぁ大丈夫なのだが,サーバクライアント間の通信遅延が大きい場合やクライアントの処理能力が非常に低い場合などはそれなりに大きなものにしておく必要があるだろう。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/dayflower/20120711/1342058487