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

Rack::Auth::Digest::MD5opaque を渡さないといけない((つうかそもそも opaque は optional なはずなのに Rack::Auth::Digest::MD5 では必須パラメータってのも変なんだけど。))ので素直に書けないと思いがちだけど,現在の 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 を返したい場合((これが本来の opaque の使い方。とはいえ,いろいろ代替手段があるので (nonce に入れ込んじゃうとか Cookie 使うとか) opaque が真面目に使われるケースはないんじゃないかな。))は,結局 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) を渡せばよくなるのでシステムに生パスワードを保存しておく必要がなくなる((実はハッシュ化された A1 (と username の組) を盗まれると認証できてしまう。なのでハッシュ化して保存したとしてもユーザーの (他のサービスと共用しているかもしれない) 生パスワードが盗まれないという意義しかない。))。

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 を破棄していけば,その一定時間を超えた段階でのリプレイ攻撃が成立しなくなるのでカジュアルには,悪い選択肢ではない*1

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 をもとに自動的に認証ダイジェストを計算して再送信することになる((なので time_limit をかなり小さくしてもまぁ大丈夫なのだが,サーバとクライアント間の通信遅延が大きい場合やクライアントの処理能力が非常に低い場合などはそれなりに大きなものにしておく必要があるだろう。))。

と,これでいいはずなんだけど,現在のところ 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:とはいえ当然ながら「一定時間」内でのリプレイ攻撃は成立してしまうので,攻撃者が盗聴可能な経路でこのような実装を利用するのはやめたほうがよい。