以下の URL では Digest 認証を行っています。
% telnet X68000.q-e-d.net 80 GET /~68user/net/sample/http-auth-digest/secret.html HTTP/1.0 (リターン) Host: X68000.q-e-d.net:80 (リターン) (続けてリターン) HTTP/1.1 401 Authorization Required Date: Fri, 03 Dec 2004 15:43:35 GMT Server: Apache/2.0.52 (FreeBSD) WWW-Authenticate: Digest realm="Secret Zone", nonce="RMH1usDrAwA=6dc290ea3304de42a7347e0a94089ff5912ce0de", algorithm=MD5, qop="auth" Content-Length: 471 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>401 Authorization Required</title> </head><body> <h1>Authorization Required</h1> <p>This server could not verify that you are authorized to access the document requested. (略)Basic 認証では
WWW-Authenticate: Basic realm="Secret Zone"だったものが、Digest 認証では
WWW-Authenticate: Digest realm="Secret Zone", nonce="RMH1usDrAwA=6dc290ea3304de42a7347e0a94089ff5912ce0de", algorithm=MD5, qop="auth"とかなり長くなっています。まず、
WWW-Authenticate: Digest ...となっていることから、Digest 認証であることがわかります。後に続く値の意味を解説します。
qop="auth,auth-int"といような書き方もありえます。
ただし、この解説では「auth」のみを考慮します。
GET /~68user/net/sample/http-auth-digest/secret.html HTTP/1.1 Authorization: Digest username="hoge", realm="Secret Zone", nonce="RMH1usDrAwA=6dc290ea3304de42a7347e0a94089ff5912ce0de", uri="/~68user/net/sample/http-auth-digest/secret.html", algorithm=MD5, qop=auth, nc=00000001, cnonce="e79e26e0d17c978d", response="0d73182c1602ce8749feeb4b89389019" Host: X68000.q-e-d.netこれまた長いので、ひとつずつ解説します。
GET URI HTTP/1.0というリクエストによってサーバに URI が通知されるので、これは冗長な情報です。 しかし、クライアントが proxy サーバ経由でサーバにアクセスしている場合、 クライアントは proxy サーバに
GET http://host/path/file HTTP/1.0というリクエストを送り、それを受けた proxy サーバは web サーバに
GET /host/path/file HTTP/1.0というリクエストを送るため、URI が最終的に変化してしまいます。 URI が変わるとメッセージダイジェストの値も変わるので、認証が通らなくなってしまいます。 よって、わざと冗長な情報を Authorization ヘッダに含めているのです。
request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) <"> A1 = unq(username-value) ":" unq(realm-value) ":" passwd A2 = Method ":" digest-uri-valueさらに、以下のような説明があります (以下の訳は StudyingHTTP の RFC 2617 日本語訳 を参考にさせていただきました)。
"MD5" 及び "MD5-sess" アルゴリズムにおいて
H(data) = MD5(data)であり、また
KD(secret, data) = H(concat(secret, ":", data))である
response の値は
「A1のMD5値 + ":" + nonce値 + ":" + nc値 + ":" cnonce値 + ":" + qop値 + ":" + A2のMD5値」の MD5 値です。この A1 とは
「username + ":" + realm + ":" + passwd」の MD5 値のことで、A2 は
「Method + ":" + uri値」(Method は HTTP の GET や POST のこと)です。いずれの値も両端をダブルクォートで囲みません。
こういうものは説明するよりコードを示した方が早いですね。
#!/usr/bin/perl # Digest::MD5 を使用する。 use Digest::MD5 qw(md5_hex); # あらかじめ or リクエストを送信するときまでにわかっている値 $username ='hoge'; $passwd ='fuga'; $method = 'GET'; $uri = "/~68user/net/sample/http-auth-digest/secret.html"; # サーバ側が生成する値 $nonce = "RMH1usDrAwA=6dc290ea3304de42a7347e0a94089ff5912ce0de"; $realm = 'Secret Zone'; $qop = 'auth'; # クライアント側が生成する値 $nc = '00000001'; # nonce-count $cnonce = 'e79e26e0d17c978d'; # ランダム文字列 # response を生成 $a1 = "$username:$realm:$passwd"; $h_a1 = md5_hex($a1); $a2 = "$method:$uri"; $h_a2 = md5_hex($a2); $response = "$h_a1:$nonce:$nc:$cnonce:$qop:$h_a2"; $h_response = md5_hex($response); # 結果表示 print "\$a1=$a1\n"; print "\$h_a1=$h_a1\n"; print "\$a2=$a2\n"; print "\$h_a2=$h_a2\n"; print "\$response=$response\n"; print "\$h_response=$h_response\n";
$h_a1 = md5_hex($a1);を
chomp($h_a1 = `printf "%s" "$a1" | md5`);というふうに外部コマンドを使うように書き換えるとよいでしょう (もし md5 コマンドがないなら md5sum を試してください)。 なお、外部コマンドを使う書き方は、悪意のある web サーバにダブルクォートを含む realm を送られたりすると、クライアント側で自由なコードの実行を許してしまいます。 あくまでテスト目的で行ってください。
1: #!/usr/local/bin/perl -w 2: 3: # $Id: http-client-auth-digest.pl,v 1.2 2005/02/19 16:01:53 68user Exp $ 4: # 5: # Digest 認証 手抜きクライアント。Digest::MD5 モジュール必須。 6: # 7: # written by 68user http://X68000.q-e-d.net/~68user/ 8: 9: use strict; 10: use Socket; 11: use Digest::MD5 qw(md5_hex); 12: 13: my $host = 'X68000.q-e-d.net'; 14: my $uri = '/~68user/net/sample/http-auth-digest/secret.html'; 15: my $username = 'hoge'; # ユーザ名 16: my $passwd = 'fuga'; # パスワード 17: my $method = "GET"; 18: 19: my $connect_host = $host; 20: my $port = getservbyname('http', 'tcp') || 80; 21: my $iaddr = inet_aton($connect_host) || die "$connect_host は存在しないホストです。\n"; 22: my $sock_addr = pack_sockaddr_in($port, $iaddr); 23: 24: socket(SOCKET, PF_INET, SOCK_STREAM, 0) || die "ソケット を生成できません。\n"; 25: connect(SOCKET, $sock_addr) || die "$connect_host の ポート $port に接続できません。\n"; 26: select(SOCKET); $|=1; select(STDOUT); 27: 28: my $request = ''; 29: $request .= "$method $uri HTTP/1.0\r\n"; 30: $request .= "Host: $host\r\n"; 31: $request .= "\r\n"; 32: 33: print "--- 1st requst ---\n$request---\n"; 34: print SOCKET $request; 35: 36: my %auth_info; 37: while (<SOCKET>){ 38: print "1st response: $_"; 39: if ( m/^WWW-Authenticate: Digest (.*)/i ){ 40: # カンマで分割 (手抜き) 41: foreach (split(",",$1)){ 42: s/^\s*//; # 先頭の空白を削る 43: s/\s*$//; # 末尾の空白を削る 44: my ($key, $value) = m/(.*?)=(.*)/; 45: $value =~ s/^\"//; # 先頭のダブルクォートを削る (手抜き) 46: $value =~ s/\"$//; # 末尾のダブルクォートを削る (手抜き) 47: print " [$key]=[$value]\n"; 48: $auth_info{$key}=$value; 49: } 50: } 51: } 52: close(SOCKET); 53: 54: # response 生成 55: my $nc = '00000001'; # 決め打ち 56: my $cnonce = 'e79e26e0d17c978d'; # ランダムな文字列にすべきだが、ここでは決め打ち 57: 58: my $a1 = "$username:$auth_info{realm}:$passwd"; 59: my $h_a1 = md5_hex($a1); 60: my $a2 = "$method:$uri"; 61: my $h_a2 = md5_hex($a2); 62: my $response="$h_a1:$auth_info{nonce}:$nc:$cnonce:$auth_info{qop}:$h_a2"; 63: my $h_response = md5_hex($response); 64: 65: print "a1=[$a1]\n"; 66: print "h_a1=[$h_a1]\n"; 67: print "a2=[$a2]\n"; 68: print "h_a2=[$h_a2]\n"; 69: print "response=[$response]\n"; 70: print "h_response=[$h_response]\n"; 71: 72: # 2回目の接続 73: 74: socket(SOCKET, PF_INET, SOCK_STREAM, 0) || die "ソケット を生成できません。\n"; 75: connect(SOCKET, $sock_addr) || die "$connect_host の ポート $port に接続できません。\n"; 76: select(SOCKET); $|=1; select(STDOUT); 77: 78: $request = ''; 79: $request .= "$method $uri HTTP/1.0\r\n"; 80: $request .= "Host: $host\r\n"; 81: $request .= qq(Authorization: Digest username="$username",); 82: $request .= qq( realm="$auth_info{realm}",); 83: $request .= qq( nonce="$auth_info{nonce}",); 84: $request .= qq( uri="$uri",); 85: $request .= qq( algorithm=$auth_info{algorithm},); 86: $request .= qq( response="$h_response",); 87: $request .= qq( qop=auth, nc=$nc,); 88: $request .= qq( cnonce="$cnonce"\r\n); 89: $request .= "\r\n"; 90: 91: print "\n--- 2nd requst ---\n$request---\n"; 92: print SOCKET $request; 93: 94: while (<SOCKET>){ 95: print "2nd response: $_"; 96: } 97: close(SOCKET);
#LoadModule auth_digest_module libexec/apache2/mod_auth_digest.soのコメントを外して有効にし、Apache を再起動する必要があります。
ただし、IE6 では Digest 認証を使って foo.cgi?FOO=BAR のような URL にリクエストすると、 誤ったリクエストを送ってしまうバグがあります。 以下の URL は Mozilla や FireFox では正しく閲覧できますが、IE6 だと正しいユーザ名・パスワードを送信しても、 400 Bad Request になってしまいます (Windows XP SP2 + IE6 で確認)。
[error] [client 192.168.1.11] Digest: uri mismatch - </~68user/net/sample/http-auth-digest/secret.cgi> does not match request-uri </~68user/net/sample/http-auth-digest/secret.cgi?FOO=BAR>これは IE のバグですが、いつまでたっても修正されないので、 Apache-2.0.51 からサーバ側で無理矢理対応する仕組みが設けられました。 httpd.conf や .htaccess に以下のように記述しておけば、 IE でも上記のような URL に正しくアクセスできます。
# User-Agent ヘッダに MSIE という文字列が含まれていたら、 # 環境変数 AuthDigestEnableQueryStringHack に「On」をセットする。 BrowserMatch "MSIE" AuthDigestEnableQueryStringHack=On上記の設定がなされていた場合、まず正規の方法で認証を行い、 それに失敗した場合に IE 用の認証処理が行われます。 よって、今後 IE のバグが修正されたり、IE 以外のブラウザの User-Agent に「MSIE」という文字列が含まれていたとしても、問題は起きないと思われます。
なお、IE7 ではこのバグは修正されています。
まず、qop の値である auth や auth-int は token です。token はダブルクォートで囲む必要はありません。
しかし、サーバが出力する WWW-Authentication ヘッダの qop には複数の値が入る可能性があり、 その場合はカンマ区切りにしなければならないので qop="auth,auth-int" や qop="auth" のように常にダブルクォートで囲って「文字列」として扱います。
しかしクライアントが送る Authorization ヘッダの qop は、サーバが提示した qop の中からひとつを選んで送り返すので、ここでの qop の値は token となります。 よって、クライアントが送る qop はダブルクォートを付けてはいけないのです。
% htdigest -c .htdigest "Secret Zone" hoge New password: (fuga と入力) Re-type new password: (fuga と入力) Adding password for user hoge
ご意見・ご指摘は Twitter: @68user までお願いします。