さくらのVPSにSMTPサーバを立てたんですが、SMTP-AUTHは暗号化しなきゃだめだろって事で、調子に乗ってCRAM-MD5にしたらQdsmtpのSMTP-AUTHはPLAINのみの対応でした・・・orz
そこで今回は、QdmailをちょこっとイジってCRAM-MD5に対応させる方法を紹介します。
CRAM-MD5の場合ChallengeCode(以下、チャレンジコードと表記)取得のため2回に分けてサーバと通信しなければいけないことから、PLAIN認証の場合と処理手順が少し変わります。今回のTipsではcommunicateメソッドと、それを呼び出すtryUntilSuccessメソッドをスルーしています。*1
本題に入る前に、CRAM-MD5について少し説明します。(僕も復習のためおさらい)
CRAM-MD5とは
SMTP-AUTHのPLAIN認証では、平文でIDとパスワードがサーバに送られますが、そりゃまずいでしょって事で生まれたのがパスワードをハッシュ化して生のパスワードをネットワークに流さない方法でした。
CRAM-MD5はその方法のひとつで、SMTPサーバにチャレンジコード(タイムスタンプとホスト名で構成される)を依頼 ⇒ 受け取ったチャレンジコードとパスワードからハッシュを作成して認証を行います。
パスワードはその時点でサーバから受け取ったチャレンジコードを利用してハッシュ化されるためにネットワークを流れることは無いというわけです。
ただし、CRAM-MD5で利用されるMD5というハッシュ関数に脆弱性が見つかったためにSMTPを踏み台にされる可能性はゼロではありません。ニュアンスとしてはPLAINよりも幾分マシ。といった程度なのかもしれませんが、平文でパスワードが流れるよりはいいよね(素人考えだけど)。ということにしましょう。
やり方
さて、本題に入りましょう。
修正するファイルはQdsmtp.phpです。
1.smtp_auth_kindプロパティにCRAM-MD5を追加する
QdsmtpBaseクラスのプロパティを見ると、$smtp_auth_kindが定義されていますが、よくみるとコメントアウトされている行にCRAM-MD5の文字が。いずれ対応予定だったのかもしれませんね。
var $smtp_auth_kind = array('PLAIN'); // var $smtp_auth_kind = array('CRAM-MD5','DIGEST-MD5','LOGIN','PLAIN');
今回はこの設定を利用してCRAM-MD5の認証処理を作っていきます。
まずは、$smtp_auth_kindにCRAM_MD5を追加しましょう。
var $smtp_auth_kind = array('PLAIN'); ↓ var $smtp_auth_kind = array('CRAM_MD5', 'PLAIN');
2.cram_md5メソッドを作成
Qdsmtpのメール送信処理は、send() -> sendBase()というふうに流れますが、このsendBaseメソッドから$smtp_auth_kindで定義した認証方式のメソッドを呼び出しています。
呼び出されるメソッドはstrtolower関数を通されるのでPLAINならplainメソッド、CRAM_MD5ならcran_md5メソッドが呼ばれます。
それでは、cram_md5メソッドを作成しましょう。
//-------------------------- // AUTH //-------------------------- function plain(){ // 略 } function makePlain(){ // 略 } // CRAM-MD5 function cram_md5() { // ここに作成する }
このときcram-md5というメソッド名にするとeclipse上でパースエラーが発生しました。定義済みなの?実際に動作させてないのでわかりませんが、気持ちが悪いのでcram_md5にするために$smtp_auth_kindの設定をCRAM_MD5にしたわけです。
cram_md5メソッドはplainメソッドをお手本に、認証コードの作成(と通信処理も書いてしまう)をmakeCramMd5メソッドに渡すことにしましょう。
function cram_md5() { $cram_md5 = $this->makeCramMd5(); // makeCramMd5メソッドを呼ぶ(処理もこのメソッドに任せるので返り値は使わない) return true; // 最初に書いた理由でtryUntilSuccessをスルーするのでtrueを返してしまう }
3.makeCramMd5メソッドを作成する
cram_md5メソッドのすぐ下にmakeCramMd5メソッドを作成します。makePlainメソッドは、通信に利用するコードのみを生成していますがCRAM-MD5認証だとtryUntilSuccess() -> communicate() に対応できないため、ここで行う通信処理も一緒に書いてしまいます。
それではコード上で説明していきますね。
function makeCramMd5() { // 認証方式送信 fwrite($this->sock,"AUTH CRAM-MD5\r\n"); // Challengeコード取得(Challengeコード:タイムスタンプ.ホスト名 で構成される) $ccLine = fgets($this->sock); // ChallengeCodeLine // 取得した文字列"334 *****************"ステータスコード?とチャレンジコード部分を分けるため半角スペースでexplode $in = explode(" ", $ccLine); // in[0]ステータスコードが334なら if($in[0] == '334'){ // チャレンジコードをbase64デコード $ticket = base64_decode($in[1]); // 上記チケットとパスワードをmd5でHMAC方式でハッシュ化する $password = hash_hmac('md5', $ticket, $this->smtp_param['PASS'], false); } // id(メールユーザ名)とハッシュ化したパスワードを半角スペースでつないでbase64エンコード。$cram_md5を配列にしているのはmakePlainメソッドに合わせてる $cram_md5[0] = base64_encode($this->smtp_param['USER']." ".$password); // 認証しちゃう fwrite($this->sock, $cram_md5[0]."\r\n"); $authResult = fgets($this->sock); return $cram_md5; }
とまあこんな感じ。
コメントで説明し尽くしているので、何も言うことはありません。これでSMTP-AUTHのCRAM-MD5が利用できるようになりました。
ここに書いた方法を応用してDIGEST-MD5に対応させることもできると思います。
また、「SMTPS + SMTP AUTH」や「SMTP + TLS + SMTP AUTH」など、プロトコルを追加することも可能だと思います。
僕もいずれはチャレンジしてみたいと思いますが、やってみた方はぜひ教えてくださいね^^
あとがき
今回は色々なサイトを参考にさせていただきながら実装したことで、SMTPの仕組みについてかなり新しい知識を入れる事ができました。PLAINってかなりヤバいんじゃね?とか、CRAM-MD5にしてもSMTPサーバからコードを取得して、そのコードを利用してパスワードをハッシュ化、認証という流れにもかかわらず、それでも脆弱性がある理由とか・・・。そして今のトレンドはSMTP over SSL(STARTTLS)ってことも。
この作業は昨夜3時間くらいかけて行ったんですが、作業を始める時点でSMTPに関する知識はほとんどありませんでしたが、作業を通じて、この記事を書く過程で理解を深める事ができました。
最後に、QdsmtpをCRAM-MD5に対応させるに当たって参考にさせていただき、知識を深めるのに役立ったサイトを時系列で紹介していきますね。
SMTPに関する知識を得たサイト
メールのプログラムについて(主に初級者向け)
メールの技術情報について僕のような素人にもわかりやすく解説してくれています。実装するにあたってもCRAM-MD5のSMTPサーバとのやり取りの解説が参考になりました。
公開と隠ぺいのジレンマ「SMTP?後編」
SMTP-AUTHの認証方式(メカニズムと書いてある)について解説です。
実装にあたって参考にさせていただいたサイト
PHPでCRAM-MD5認証を行ってOutbound Port 25 Blocking(OP25B)を回避してみる – eth0jpの日記
PHPによるCRAM-MD5を利用したメール送信のサンプルソースを公開してくれています。このページが無ければ実装するのはかなり困難だったでしょう。ありがとうございます!
Php: Smtp-auth Cram-md5
海外のLinux関連のフォーラムのようです。CRAM-MD5のための通信コードを得る簡潔なサンプルコードでこちらも役に立ちました。hash_hmacの引数の順番が間違っているのがお茶目ですね(笑)
ステータスコード
SMTP command reference
実装にあたってはコード530に悩まされました…。エラーはcommunicateメソッドを通していたことで、正しくない通信コードを送っていた事がその原因でしたが、その解決の一助となったかな?さらに高度なことをするのであればとても役立つページです。
セキュリティについて
各プロトコルの安全性
プロトコル別のセキュリティについて言及されています。CRAM-MD5のようなやり方でも脆弱性を持ってしまうんですね。もっと勉強しないと。
PHPのメール関数とQdmail、そしてSMTP通信の違い
SMTP送信について – Qdmail – PHP::Mail Library , Quick and Detailed for Multibyte
いつも利用させていただいているQdmailの公式サイトの記述です。SMTP送信について非常にわかりやすく解説してくれています。
スペシャルサンクス
PHP高機能日本語メール送信ライブラリ・文字化けフリー – Qdmail – PHP::Mail Library , Quick and Detailed for Multibyte
いわずと知れたQdmailの公式ページです。spokさま、素晴らしいライブラリをありがとうございます!以前はメールをやり取りさせていただいたこともあるのですが、本当に親切に対応していただいた覚えがあります。最近は動向をうかがい知る事ができませんが、影ながらご活躍を応援しております!
- PLAIN認証と同じようにCRAM-MD5認証の処理を作るにはommunicateメソッドで行う$put_message(SMTPサーバに送るメッセージ)の作成形式に合わないので、この部分を修正する必要がありました。そこで新たに作成したmakeCramMd5メソッドでSMTPサーバとの通信処理を完結させるようにしました。 [↩]