«前の日記(2006年03月08日) 最新 次の日記(2006年03月10日)» 編集

ema log


2006年03月09日 [長年日記]

_ [Programming][apache][Linux] Digest 認証の仕組み

MD5 って何?とかって人には「暗号技術入門-秘密の国のアリス」をオススメします。*1

まずは telnet

HTTP クライアントを作ってみよう(6) - Digest 認証編 - を片手に、なにはともあれ telnet で直接流れを確認。

$ telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /~munehisa/diary/update.rb HTTP/1.0

HTTP/1.1 401 Authorization Required
Date: Wed, 08 Mar 2006 15:05:02 GMT
Server: Apache/1.3.33 (Unix)  (Vine/Linux) PHP/5.1.2 mod_gzip/1.3.26.1a mod_fastcgi/2.4.2 mod_ssl/2.8.22 OpenSSL/0.9.7d
WWW-Authenticate: Digest realm="tDiary-Digest", nonce="nvIORA==4848b82bbd2b11254998251199d581fb2da5eb4f", algorithm=MD5, qop="auth"
Connection: close
Content-Type: text/html; charset=iso-8859-1

要約だけ抜き出していますが、重要なのは WWW-Authenticate ヘッダです。

WWW-Authenticate: Digest realm="tDiary-Digest", nonce="nvIORA==4848b82bbd2b11254998251199d581fb2da5eb4f", algorithm=MD5, qop="auth"

この情報を元にユーザがダイジェストを計算して

GET /~munehisa/diary/update.rb HTTP/1.1
HOST: ema.fsr.jp
Authorization: Digest username="AAAAA", realm="tDiary-Digest", nonce="XXXXX", uri="/~munehisa/diary/update.rb", algorithm=MD5, qop=auth, nc=00000001, cnonce="d9bd899401c196c29999f5ae5355183d", response="YYYYYY"

の様なリクエストを再度送ることで認証します。

これ以上は、ただのまる写しにしかならないので「HTTP クライアントを作ってみよう(6) - Digest 認証編 -」をオススメします。

そもそも認証の流れは?

では、本題。

まずは、Basic 認証の流れを図に示します。Digest 認証と Basic 認証で異なる部分は図の赤枠で囲った部分になります。Basic 認証だとパスワードも平文で流れるため、盗聴者は簡単にパスワードそのものを入手できてしまいます。

通信の全体を暗号化するわけではなく、あくまでもパスワードを平文で送らないようにする事が Digest 認証の狙いです。通信内容そのものを暗号化したい場合、SSL や TLS を使用する必要があります。Digest 認証 を通過したからといって、クレジットカードの情報などを送信すると、その情報は盗聴されるおそれがあります。

というのも、リクエストの Header が変わるだけで、リクエストの Body は変わらないからです。

さて、実際の処理の説明に移ります。

Digest 認証の実際

まず、ユーザ hoge から認証が必要な update.rb への GET リクエストが送信されました。ユーザは以下の通りとします。

ユーザ名
hoge
パスワード
foo
メソッド
GET
URI
/update.rb

サーバは、example への Digest 認証が必要なことをヘッダに含めたレスポンスを返します。そのヘッダ部分を抜き出すと、例えば、次のようになります。

WWW-Authenticate: Digest realm="example", nonce="nvIORA==4848b82bbd2b11254998251199d581fb2da5eb4f", algorithm=MD5, qop="auth"

見にくいので分解すると以下のようになります。

realm
example
nonce
nvIORA==4848b82bbd2b11254998251199d581fb2da5eb4f
algorithm
MD5
qop
auth

realm*2 は何に対する認証なのかを表します。Basic 認証でも一応存在しましたが、ただの名前でした。しかし、Digest 認証では認証情報の一部として利用されます。そこで、htdigest コマンドなどでサーバに認証情報を保存する際に指定した realm と .htaccess に AuthName で記述するなどして指定する realm の値を合わせなくてはなりません。

nonce*3 はサーバで認証のために生成されたランダムな文字列です。

algorithm は使用するアルゴリズムです。MD5 か MD5-sess のいずれかになるようです。MD5-sess が指定された場合、後述の A1 の計算方法が変わります*4。今回は MD5 についてのみを扱います。

qop は「サーバによって提供される "保護の品質" の値」とされています。auth か auth-int をとり、後述する A2 の計算方法が変わります*5。今回は auth についてのみを扱います。*6

ユーザはこれらヘッダに含まれる情報と、ユーザ名、パスワードなどを使用して認証情報を生成します。認証情報にはダイジェストとパスワードを除くダイジェストの計算に使用した値が含まれます。以下に計算の流れを下図に示します。なお、図中で Request-Digest の部分で : ごとに改行していますが、これは説明および表記上の都合で、実際には改行されてはいないことに注意してください。

ユーザはダイジェストを作るために全部で3回ハッシュを計算します。ここではハッシュ関数に MD5 *7 を用いています。

まずは「ユーザ名:realm:パスワード」という文字列のハッシュを計算します。今回は「ユーザ名: hoge、パスワード: foo、realm: example」ですので「hoge:example:foo」のハッシュを計算します。図では A1 というラベルをつけていますが、これは RFC 2617 に倣ったものです。この値は後ほど使います。Ruby で求めてみたところ

983856234334d25cbfbc8ce2ff3bd2a9

となりました。

次に「リクエストしたメソッド:リクエストした URI」いう文字列のハッシュを計算します。今回は GET で /update.rb にアクセスしているので「GET:/update.rb」のハッシュを計算します。図では A2 というラベルをつけています。こちらは

1ef9015732a632939ba9f9415e294326

となりました。

そして、最後に、A1, A2 とサーバからもらった nonce、qop と nonce を使用した回数 nonce-count、任意の乱数である cnonce を用いて「A1:nonce:nonce-count:cnonce:qop:A2」という文字列のハッシュを計算します。今回は「nonce-count: 00000001、cnonce: d9bd899401c196c29999f5ae5355183d」とします。すなわち

983856234334d25cbfbc8ce2ff3bd2a9:nvIORA==4848b82bbd2b11254998251199d581fb2da5eb4f:00000001:d9bd899401c196c29999f5ae5355183d:auth:1ef9015732a632939ba9f9415e294326

となります。そのハッシュは

17de1f1504c58be01fb35093c415e4a4

です。これが認証で使用されるダイジェストです!

nonce-count
00000001
cnonce
d9bd899401c196c29999f5ae5355183d

さて、以上の計算でダイジェストを生成できました。後は、ユーザから再度、認証情報をつけたリクエストを行います。具体的には例えば以下のような Authorization ヘッダをリクエストにつけます。ここでも見やすさのために改行を付加していますが、実際には1行です。

Authorization: Digest username="hoge", realm="example",
  nonce="nvIORA==4848b82bbd2b11254998251199d581fb2da5eb4f",
  uri="/update.rb", algorithm=MD5,
  qop=auth, nc=00000001, cnonce="d9bd899401c196c29999f5ae5355183d",
  response="17de1f1504c58be01fb35093c415e4a4"

パスワードを除く情報が渡されていることがわかります。

サーバによる認証

最後に、このヘッダを含むリクエストを受けたサーバは、どのように認証すればいいのでしょうか?。A2 に必要な情報が含まれていますが、パスワードはユーザから渡されていないために A1 の計算はできません。

ここで、サーバに認証情報を保存する方法を思い出してください。ユーザ名・realm・パスワードから求めた情報を保存していました。そうです、先ほどダイジェストの計算で用いていた A1 と全く同じ組の情報です。結論を書くと、サーバには A1 の値が保存されています。

試しに「ユーザ名: hoge、realm: example、パスワード: foo」として htdigest コマンドを実行すると

$ htdigest digest_test example hoge
Changing password for user hoge in realm example
New password:
Re-type new password:
$ cat digest_test
hoge:example:983856234334d25cbfbc8ce2ff3bd2a9

となり、先ほど計算した A1 の値 983856234334d25cbfbc8ce2ff3bd2a9 と一致していることが確認できます。サーバは記録された A1 の値とリクエストに含まれる情報からダイジェストを計算し、リクエストのダイジェストと比較して、一致すれば認証が行えたことになります。

まとめ

長くなりましたが、ダイジェスト認証の大まかな仕組みは以上のようになっています。

もう少し、原理面で書き足りないことがあるのでそれをまたかこうと思います。

  • パスワードを渡すのと何が違うのか?
  • 攻撃する場合、どのような方法があるのか

といったことについて、書いてみて自分の理解を確認したいと思います。

References
HTTP クライアントを作ってみよう(6) - Digest 認証編 -HTTP 認証: 基本アクセス認証及びダイジェストアクセス認証 (RFC 2617 邦訳)

*1 結城 浩さんの Ruby 本とか本当に出たりするんだろうか。rubyco(るびこ)の日記 というのを書かれているようなので非常に楽しみ。

*2 realm のアルクでの検索結果

*3 nonce のアルクでの検索結果

*4 MD5-sess のsess は恐らく session の略で、セッションキーを利用して、クライアントの特定のみを行う場合に使用するっぽいです

*5 auth-int は authentication with integrity protection の略で、HTTP の entity-body のハッシュを認証に使用するようになります。本体部分をハッシュをに含めることで改ざんに対する耐性をあげようとしているってこと?理解できていません OTL

*6 なお、省略も可能で、その場合、ダイジェストの計算方法も変わりますが、使用すべきであるとされています。

*7 現在、MD5 はあまり堅牢ではないとされていて、SHA1 などを使用するのが一般的になっていると思うのですが、Digest 認証の規格では MD5 の使用しか規定されていないようです。もっとも、Linux の shadow パスワードも MD5 だったと思うのですが。