APOP認証
この節ではAPOP認証とその実装について解説します。
APOP認証の仕組み
まず、APOP認証の流れをざっと説明しましょう。次の図を見ながら考えてください。

APOPの認証は以下の手順で進みます。
- まずサーバは、接続ごとにユニークな文字列である「APOPスタンプ」を発行します。
- クライアントは渡された「APOPスタンプ」を使いパスワードを加工して「ダイジェスト」を作り、それをサーバに渡します。
- サーバも同様の方法でパスワードを加工してダイジェストを作り、それがクライアントが返したダイジェストと一致すれば、クライアントを認証します。
クライアントは接続ごとに異なるAPOPスタンプを使ってパスワードを加工するので、ダイジェストも接続ごとに変化します。また、パスワードを加工するときにはMD5という仕組みが使われており、ダイジェストから元の文字列を計算するのは非常に困難です。ですから、もし通信内容をまるごと悪人に見られたとしても、パスワードを推測するのは難しいわけです。
もっとも、APOPもいいことばかりではありません。一番の問題は、サーバがパスワードを平文で保存しなければいけないことです。通常の認証方法を使う場合には、サーバはパスワードを暗号化して保存できます。それと比較すると、APOPを使うことで通信中にパスワードを盗まれる危険は減りますが、サーバ側のセキュリティリスクが高まることになります。
認証前の流れ
では、ここからはAPOP認証の実装について解説していきます。再度POPd#_sessionメソッドを見てください。
def _session stamp = apop_stamp() print_line "+OK popd #{stamp}" mailbox = auth(stamp) while cmd = read_cmd() mid = "cmd_#{cmd.name}" unless respond_to?(mid, true) print_line '-ERR unknown command' next end __send__(mid, mailbox, cmd.args) end end
このメソッドのうち、APOP認証に関係があるのは最初の3行だけです。残りは認証が済んだあとのコードなので、当面無視してください。
最初の3行のうち、まだ説明していないメソッドはapop_stampとauthです。apop_stampメソッドはAPOPスタンプを作成します。authメソッドは実際の認証を行います。
上記のコードでは、まずapop_stampメソッドでAPOPスタンプを作り、print_lineメソッドでそれを含めた最初の反応(+OK ……)を返します。そしてauthメソッドでクライアントを認証します。
メソッドの定義を順番に見ていきましょう。
POPd#apop_stampメソッドの定義
POPd#apop_stampはAPOPスタンプを生成するメソッドです。POPd#apop_stampメソッドのコードを以下に示します。
def apop_stamp thread_id = sprintf('%x', Thread.current.object_id) "<#{Time.now.to_i}.#{Process.pid}.#{thread_id}@#{Socket.gethostname}>" end
APOPスタンプは、メールで使われるメッセージID(message ID)と同じ形式の文字列で、接続のたびに違う文字列にならなければいけません。そこで、毎回値を変えるために、以下の3つの値を利用します。
- 時刻(
Time.now.to_i) - プロセスID(
Process.pid) - スレッドID(
Thread.current.object_id)
さらに、APOPスタンプは違うホスト上では違う値にならなければいけないので、POPサーバが動作しているホストのホスト名も使います。サーバが動作しているホストの名前を得るには、Socket.gethostnameメソッドが使えます。
以上の値をすべて含む文字列を生成すれば、ホストごとにユニークで、かつ接続のたびに違う文字列が得られます。
POPd#authメソッドの定義
次に、実際に認証を行うPOPd#authメソッドを説明します。POPd#authメソッドのコードを以下に示します。
# support only APOP def auth(stamp) while cmd = read_cmd() case cmd.name when 'APOP' begin account = auth_APOP(stamp, cmd.args) print_line '+OK' return account.mailbox rescue => err print_line "-ERR #{err.message}" end when 'QUIT' print_line '+OK bye' terminate else print_line '-ERR use apop' end end terminate end
このように、POPd#authメソッドではwhileループでPOPコマンドを読み込みながら、クライアントが送ったコマンドを順番に処理していきます。認証前の段階で使ってよいPOPコマンドは、APOP、QUIT、それから通常の認証に使うUSERだけです。ただし、POPdではUSERには対応しないので無視して-ERRを返します。それ以外のコマンドにもすべて-ERRを返します。
実際にAPOP認証を行っているのは、「when 'APOP'」の節、さらに言えばauth_APOPメソッドです。auth_APOPメソッドは、認証が成功したら、APOPコマンドで指定されたユーザーに対応するPOPd::Accountオブジェクトを返します。認証に失敗した場合は、例外POPd::AuthErrorが発生します。
認証が成功したら、クライアントに「+OK」を返して、authメソッド自体はユーザーのメールボックスオブジェクト(account.spool)を返します。メールボックスオブジェクトとは、具体的には、POPd::Maildirオブジェクトです。
POPd#auth_APOPメソッドの定義
さて、APOP認証の具体的な実装はすべてauth_APOPメソッドで行われています。次にPOPd#auth_APOPメソッドを見てみましょう。
def auth_APOP(stamp, args) raise SyntaxError, 'apop syntax error' unless args.size == 2 username, digest = *args account = @accounts[username] unless digest == create_digest(stamp, account.password) raise AuthError, 'wrong password' end unless $DEBUG raise AuthError, 'no mbox' unless account.mailbox_exist? account end def create_digest(stamp, password) Digest::MD5.hexdigest(stamp + password) rescue raise if $DEBUG raise ArgumentError, 'configuration error' end
APOPコマンドは「APOP ユーザー名 ダイジェスト」という構文で、引数は常に2つです。つまりauth_APOPメソッドの引数argsは、例えば["taro", "xxxxxxxxxxxxxxxxxxxx"]のような値です。
まず、@accounts[username]でユーザー名に対応するPOPd::Accountオブジェクトを得ます。ここでユーザーが存在しなかったら、例外ArgumentErrorが発生します。
次に、create_digestメソッドでサーバ側のダイジェストを作り、クライアントが送ってきたダイジェスト(digestの値)と比較します。値が同じなら認証は成功です。値が違ったら認証失敗なので、POPd::AuthErrorを発生させます。
最後に、account.mailbox_exist?でアカウントのメールボックスが実際に存在するかどうか確認して、存在しないならやはり認証は失敗とみなします。
ちなみに、本当はここでメールボックスをロックしなければいけません。しかし今回のコードでは「同じユーザが同時にアクセスすることはきっとないだろう」という希望的観測のもとにロックを省略しています。
アカウント情報の取得
POPd::AccountTableクラスとPOPd::Accountクラスについては、コードを見れば内容は分かってしまうくらい単純なので、本稿では説明を省略します。ただし、アカウント情報(POPdで使用するのはホームディレクトリのみ)の取得については、やや説明が必要だと思うので、その点だけ解説します。
POPdでは、Rubyのetcライブラリを使ってアカウントの情報を取得します。具体的には、Etc.getpwnamメソッドを使って、ユーザー名に対応するアカウント情報を取得します。例えばユーザー「taro」のアカウント情報を得るには、「Etc.getpwnam("taro")」を使います。
Etc.getpwnamメソッドの返り値はStruct::Passwd構造体です。Struct::Passwd構造体のメンバは、OSによって微妙に違いますが、少なくとも以下のメンバは共通です。
| メンバ名 | 解説 |
| name | ユーザー名 |
| passwd | 暗号化されたパスワード |
| uid | ユーザーID |
| gid | グループID |
| gecos | GECOSフィールド |
| dir | ホームディレクトリのパス |
| shell | ログインシェル |
POPdでは、パスワードを記録したファイルの場所を得るためにdirメンバ(ホームディレクトリ)だけを使っています。
