2016/05/08(日)今更ながら DIGEST-MD5 SMTP認証[RFC2831,RFC1321]/postfix 3.0.3 + dovecot 2.2.21
2017/10/12 19:24
DIGEST-MD5 は、実装難易度などからサーバ側でのサポート割合が低いのですが、昨年からの POP before SMTP のサポート淘汰に伴い、この状況は変化しているようです。
DIGEST認証は、RFC2831 にて、従前から先行して使われていた HTTP における DIGEST認証と互換を持たせる形で規定されています。
日本語でのまともな資料がなく、仕様そのものも少し中途半端な感があります。
よって、CRAM-MD5 と異なり、サーバ側の動作環境で挙動が異なる場合が考えられるので、ここでは当方の環境である Postfix 3.0.3 + dovecot 2.2.21 における環境であることを明示しておきます。
〔2016/05/09 追記〕 Postfix 3.1.0 + dovecot 2.2.24 でも大丈夫でした。
DIGEST-MD5 によるSMTP認証は、以下のようなシーケンスを取ります。
[Client -> Server] AUTH DIGEST-MD5 [Server -> Client] 334 cmVhbG09ImV4YW1wbG...《以降省略》 [Client -> Server] dXNlcm5hbWU9ImluZm8uZXhhbXBsZS5jb20i...《以降省略》 [Server -> Client] 334 cnNwYXV0aD1mOTE2MGE0NzQ4MjE3MmNlMmMxNDk2NGFjZTUyN2VjOA== [Client -> Server] ZjkxNjBhNDc0ODIxNzJjZTJjMTQ5NjRhY2U1MjdlYzg= [Server -> Client] 235 Authentication successful.認証途上でサーバとやりとりするデータは、必ず Base64エンコードして送受信します。
クライアント側では、Base64 デコードして処理しなければなりません。
DIGEST-MD5 における処理の要(かなめ)は、後述するレスポンス MD5 ハッシュの生成になります。
この部分はちょっと複雑です。
(対応した案件でもC言語のソースコードレベルで 150行前後の規模になってしまった)
順に処理を追ってみます。
1)サーバに対し、AUTH DIGEST-MD5 コマンドを発行すると、状態コード 334 で、Base64エンコードされた文字列が返される。先ずはこれをデコードする。
・図中のBase64文字列をデコードすると、下記の文字列が得られます。これを「チャレンジ」と言います。
realm="example.com",nonce="JQMKtdgbEhMra4GdAYmAjQ==",qop="auth",charset="utf-8", algorithm="md5-sess"・これらは、パラメータとその値です。
表示都合上、改行入れていますが、実際には改行しません。
・ダブルクォーテーションで括ってある文字列については、ダブルクォーテーションを除去します。
・DIGEST-MD5 SMTP 認証の場合、qop="auth" と algorithm="md5-sess" になるので、ここではこの事例のみを扱います。
・認証フェーズで使用する文字コードは、通常 utf-8 です。
このパラメータが無い場合は、iso-8859-1 と見なさなければなりません。
しかし、通常の 7bit ASCII を使う限り、utf-8 と 7bit ASCII は同じで、文字コードとして 0x20 ~ 0x7e の間しか出てこないので全く意識していません。
2)レスポンス MD5ハッシュの生成
2-1) RFC2831 で規定する A1 データ列を生成。
RFC2831 では A1 データ列は以下のように規定されています。
A1 = { H( { username-value, ":", realm-value, ":", passwd } ), ":", nonce-value, ":", cnonce-value, ":", authzid-value }ここで、
username-value = info.example.com (ユーザ名を平文で) realm-value = サーバから与えられた realm の値を複写 (example.com) passwd = userpassword (パスワードを平文で) nonce-value = サーバから与えられた nonce の値を複写(JQMKtdgbEhMra4GdAYmAjQ==) cnonce-value = lWF{[QuiRj}_L[PW (クライアント側でランダム文字列を生成する) authzid-value = サーバから与えられた authzid の値を複写としますが、通常、authzid パラメータは与えられませんので、この場合はこうしろと規定されています。
A1 = { H( { username-value, ":", realm-value, ":", passwd } ), ":", nonce-value, ":", cnonce-value }具体的には、まず username-value,realm-value,passwd を ':' で連結した文字列
info.example.com:example.com:userpasswordに対して、MD5ハッシュを算出し、
(ハッシュ値は以下の16バイトデータになります)
この 16バイトデータと、nonce-value と cnonce-value を ':' で連結し、連結した文字列の MD5 ハッシュを算出しておきます。
算出値は
となります。
2-2) A2 データ列の生成
RFC2831 では A2 データ列は以下のように規定されています。
A2 = { "AUTHENTICATE:", digest-uri-value }ここで、digest-uri-value は、サーバタイプ、ホスト名、サーバ名を '/' で連結した文字列で、サーバタイプは 'smtp'、ホスト名は DNS MXレコードで検索できるFQDN、サーバ名は通常省略します。
ここで、ホスト名を 'mx.example.com' とすると A2 データ列は以下のようになります。
AUTHENTICATE:smtp/mx.example.comA2 データ列に関しても MD5ハッシュ値を算出しておきます。
算出値は
となります。
2-3) response ハッシュ値の算出
RFC2831 では以下のように規定されています。
HEX( KD ( HEX(H(A1)), { nonce-value, ":" nc-value, ":", cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))KD は2つの文字列を':'で連結の意、
HEX はバイトデータ列を 16進文字列表記に変換、
H は、ハッシュ関数の意味で、ここでは MD5 になります。
ここで示している具体例だと、先ず
46a5d6ccc156d8ca8da970723d455d17:JQMKtdgbEhMra4GdAYmAjQ==:00000001: lWF{[QuiRj}_L[PW:auth:e7280b0554e7e6636bd6a32ec6d5d2cf※ 表示上、改行しているが、実際は絶対に改行を入れないこと。
のような連結文字列を生成し、この連結文字列に対して MD5ハッシュ値を16進数表記したものを得ます。
算出文字列は、
729303fe19230ab4d3733dd28ab2b0b2となります。
2-4) サーバに返すレスポンスデータ列の生成
具体的には以下のような文字列を生成します。
username="info.example.com",realm="example.com",nonce="JQMKtdgbEhMra4GdAYmAjQ==", cnonce="lWF{[QuiRj}_L[PW",nc=00000001,qop=auth,digest-uri="smtp/mx.example.com", response=729303fe19230ab4d3733dd28ab2b0b2,charset="utf-8"※ 表示上、改行しているが、実際は絶対に改行を入れないこと。
・qop パラメータにはダブルクォーテーションを付けないことに注意してください。
・realm パラメータ、nonce パラメータ、qop パラメータは、サーバから与えられた値をそのまま返します。
・realm 値は空文字列のことがあります。この場合もそのまま空文字列を返します。
ただ、RFC2831 では「サーバのホスト名が入る」とされています。
・時折、digest-uri パラメータのホスト名に realm の値を設定するような事例を見かけますが、これは正しくありません。DNS のMX レコードに指定されるホスト名を使用すべきと観ています。RFC2831 に説明はあるのですが 、今一つ明確ではありません。。
・nc 値は、事実上 00000001 固定です。サーバ側はこれをチェックしているため、00000001 以外の値だと認証エラーになります。
上記文字列を Base64 エンコードすると、シーケンス図の中に書いてある文字列になります。この Base64エンコード文字列をサーバに返信します。これを「レスポンス」と言います。
3)認証確認
DIGEST-MD5 はこれでユーザ認証処理完了ではなく、もう一度状態コード 334 で Base64 エンコードした文字列が送られてきます。
cnNwYXV0aD1mOTE2MGE0NzQ4MjE3MmNlMmMxNDk2NGFjZTUyN2VjOA==を Base64デコードすると、
rspauth=f9160a47482172ce2c14964ace527ec8が得られます。
この32バイト長の16進数文字列は、2)レスポンス MD5ハッシュの生成 で A2 データ列を
A2 = { ":", digest-uri-value }に変えて処理したものになります。
クライアント側は、肯定応答をするために同じように A2 データ列を上記のものに変更したレスポンスハッシュ値を算出し、サーバ側に送り返します。
このあたりは、RFC2831 に明確な記載がなく、当方の環境で確認した挙動です。
このレスポンスがあって、サーバは初めて状態コード 235 を返して認証フェーズを完了します。