24 August 2008

(Perl)ユーザがコントロール可能なセキュア認証

PHP版と同じ機能のPerl版スクリプト

HTTPSやダイジェスト認証が使えない共用レンタルサーバで、よりセキュアな認証を目指す方法。

ユーザ名は
・サーバ側から送付されたランダム・キーを付加してブラウザのJavaScriptでMD5ハッシュ生成
パスワードは
・クライアント側のブラウザのJavaScriptでMD5ハッシュ生成
サーバ側では
・既存のPEAR Auth用のサーバ側の認証DBが流用可能

という手法で、通信系路上でキャプチャされたとしても、ある程度安全が保てるのではなかろうか

ログオン用のメインスクリプト

index.cgi
#!/usr/bin/perl use strict; use CGI; require "auth_md5.pl"; my $strUser = &CheckAuth('index.cgi', 'popmailmnu'); if($strUser eq "") { print "<p>ログオンしていません<br />(ユーザ名・パスワードが間違っているか、ログオン後30分以上経過している場合も含む)</p>\n"; print "<p><a href=\"index.cgi\">ログオン画面を再表示する</a></p>\n"; print "<p><a href=\"logoff.cgi\">ログオフする</a></p>\n"; } else { # ログオン後に処理したい内容をここに書く } print "</body>\n</html>\n";

ログオフ用のスクリプト

logoff.cgi
#!/usr/bin/perl use strict; use CGI; require "auth_md5.pl"; my $strUser = &LogoffAuth(); print "Content-type: text/html\n\n"; print <<"_HEAD_HTML"; <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=shift_jis" /> <meta http-equiv="Content-Language" content="ja" /> </head> <body> <p>ログオフしました</p> <a href="index.cgi">ログオン画面に戻る</a> </body> </html> _HEAD_HTML

認証のためのインクルードPerlファイル

auth_md5.pl
#!/usr/bin/perl use strict; use CGI; use CGI::Session; require "auth_db.pl"; sub CheckAuth { my $cgi = new CGI; # 引数をローカル変数に格納 my $strReloadPage = $_[0]; # 初期値(認証されたユーザ名) my $strAuthUser_authmd5 = ''; # セッション開始 my $sid=$cgi->cookie('CGISESSID')||$cgi->param('CGISESSID')||undef; my $session=CGI::Session->new(undef,$sid,{Directory=>'/tmp'}); #セッションをクッキーとして送信 print "Set-Cookie: CGISESSID=" . $session->id . "\n"; print "Content-type: text/html; charset=EUC-JP\n\n"; print <<"_HEAD_HTML"; <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP" /> <meta http-equiv="Content-Language" content="ja" /> <title></title> <script type="text/javascript" src="/cgi-bin/utf.js"></script> <script type="text/javascript" src="/cgi-bin/md5.js"></script> <script type="text/javascript" src="/cgi-bin/authpage_form_md5.js"></script> </head> <body> _HEAD_HTML if($session->param('user') ne '') { # 既にログオン済みの場合 if($session->param('logontime') + 60*30 < time) { # ログオン後、30分以上経っている場合は強制ログオフ $session->delete(); return(""); } return($session->param('user')); } elsif(($cgi->param('user') eq "") || ($cgi->param('password') eq "")) { # 入力が不完全か、無い場合は、入力画面を表示 my $strRandSeed = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; my $strKey = ''; srand(time); for(my $i=0; $i<16; $i++) { $strKey .= substr($strRandSeed, int(rand(62)), 1); } # キーをセッションに保存 $session->param('key', $strKey); print <<"_FORM_HTML"; <form method="post" action="./$strReloadPage" name="form2"> <table border="0" cellpadding="2" cellspacing="0" align="center"> <tr><td colspan="2"><strong>ログオン送信データ</strong></td></tr> <tr><td>ユーザ</td><td><input name="user" size="40" readonly="readonly" /></td></tr> <tr><td>パスワード</td><td><input name="password" size="40" readonly="readonly" /></td></tr> <tr><td colspan="2"><input type="submit" value="ログオンする" /></td></tr> </table> </form> <form action="./$strReloadPage" name="form1"> <table border="0" cellpadding="2" cellspacing="0" align="center"> <tr><td colspan="2"><strong>ユーザ入力欄</strong></td></tr> <tr><td>ユーザ</td><td><input type="password" name="user" size="40" /></td></tr> <tr><td>パスワード</td><td><input type="password" name="password" size="40" /></td></tr> <tr><td>Key</td><td><input name="key" size="40" value="$strKey" readonly="readonly" /></td></tr> <tr><td colspan="2"><input type="button" value="MD5変換" onclick="convert_to_md5()" /><input type="reset" value="入力欄消去" /></td></tr> </table> </form> _FORM_HTML return(""); } else { # 認証を行う $strAuthUser_authmd5 = &QueryMd5User($cgi->param('user'), $cgi->param('password'), $session->param('key')); if($strAuthUser_authmd5 ne '') { # OK # ユーザ名をセッションに保存 $session->param('user', $strAuthUser_authmd5); # ログオン時刻をセッションに保存 $session->param('logontime', time); return($strAuthUser_authmd5); } } # 認証失敗 undef($session); return(""); } sub LogoffAuth { my $cgi = new CGI; # セッション開始 my $sid=$cgi->cookie('CGISESSID')||$cgi->param('CGISESSID')||undef; my $session=CGI::Session->new(undef,$sid,{Directory=>'/tmp'}); # セッションを削除 $session->clear(); $session->delete(); return(""); } ;1;

認証DBに問い合わせるインクルードPerlスクリプト

auth_db.pl
#!/usr/bin/perl use strict; use DBI; use Digest::MD5 qw(md5_hex); sub QueryMd5User { # 引数の格納 my $strUser = $_[0]; my $strPassword = $_[1]; my $strKey = $_[2]; # MySQL接続用変数 my $strSqlDsn = 'DBI:mysql:database=auth_db;host=mysqlsrv.co.jp;port=3306'; my $strSqlUser = 'auth_user'; my $strSqlPassword = 'auth_password'; # MySQL接続 my $dbh = DBI->connect($strSqlDsn, $strSqlUser, $strSqlPassword); if(!$dbh) { return(''); } # SQLクエリ文 my $sth = $dbh->prepare("SELECT user,password FROM auth_tbl"); # SQLクエリ実行 if(!$sth->execute()) { $dbh->disconnect(); return(''); } # 読み出されたデータ数が1以上であることをチェック my $nRows = $sth->rows; if($nRows < 1) { $sth->finish(); $dbh->disconnect(); return(''); } my $hashrefData; for(my $i=0; $i<$nRows; $i++) { $hashrefData = $sth->fetchrow_hashref; # 認証テーブル上の、一つ一つのユーザ・パスワードについて、同一か確認する if(md5_hex($strKey.$hashrefData->{'user'}) eq $strUser && $hashrefData->{'password'} eq $strPassword) { # ユーザ名とパスワードが一致した # MySQL切断 $sth->finish(); $dbh->disconnect(); return($hashrefData->{'user'}); } } # MySQL切断 $sth->finish(); $dbh->disconnect(); return(''); }

これらのPerlスクリプトが用いているCPANモジュールが、サーバにインストールされているか確認する。(CGI, CGI::Session, DBI,Digest::MD5 が最低でも必要)

% cpan -a ~ 必要そうなところだけ抜粋 CGI 3.15 3.40 L/LD/LDS/CGI.pm-3.40.tar.gz CGI::Session 4.14 4.35 M/MA/MARKSTOS/CGI-Session-4.35.tar.gz DBI 1.51 1.607 T/TI/TIMB/DBI-1.607.tar.gz Digest::MD5 2.36 2.36 G/GA/GAAS/Digest-MD5-2.36.tar.gz

そして、クライアント側でMD5変換するためのJavaScript関数は、Masanao Izumo氏のページよりコピーして利用させていただいた。

md5.jsutf.jsを用いる 。

authpage_form_md5.js
function convert_to_md5() { var f1 = document.form1; var f2 = document.form2; var data; // 「キー + ユーザ名」のMD5ハッシュ作成 data = utf16to8(f1.key.value + f1.user.value); f2.user.value = MD5_hexhash(data); f1.user.value = ""; // 「パスワード」のMD5ハッシュ作成 data = utf16to8(f1.password.value); f2.password.value = MD5_hexhash(data); f1.password.value = ""; }