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 = "";
}