[インデックス 10796] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/smtp
パッケージに、CRAM-MD5認証メカニズムを追加するものです。これにより、SMTPクライアントがサーバーに対してよりセキュアな方法で認証を行えるようになります。
コミット
commit 58b97a29fd5d8ad219a34b3c859842cc29d46666
Author: Vadim Vygonets <unixdj@gmail.com>
Date: Wed Dec 14 17:17:25 2011 -0500
net/smtp: add CRAM-MD5 authentication
R=golang-dev, edsrzf, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/5451087
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/58b97a29fd5d8ad219a34b3c859842cc29d46666
元コミット内容
net/smtp: add CRAM-MD5 authentication
このコミットは、net/smtp
パッケージにCRAM-MD5認証機能を追加します。
変更の背景
SMTP (Simple Mail Transfer Protocol) は、電子メールの送信に使用されるプロトコルです。初期のSMTPには認証メカニズムがありませんでしたが、スパムや不正利用の問題に対処するため、SMTP-AUTH (SMTP Authentication) が導入されました。SMTP-AUTHは、クライアントがメールサーバーに接続する際に、ユーザー名とパスワードを提示して認証を行うためのフレームワークを提供します。
SMTP-AUTHにはいくつかの認証メカニズム(SASLメカニズム)が存在します。最も単純なものはPLAIN認証ですが、これはユーザー名とパスワードを平文で送信するため、盗聴に対して脆弱です。よりセキュアな認証方法が求められる中で、CRAM-MD5のようなチャレンジ-レスポンス方式の認証メカニズムが開発されました。
このコミットの背景には、Go言語の net/smtp
パッケージが提供する認証機能の強化と、よりセキュアなメール送信のニーズに応える目的があります。特に、パスワードをネットワーク上で平文で送信することなく認証を完了できるCRAM-MD5のサポートは、セキュリティ要件の高いアプリケーションにとって重要です。
前提知識の解説
SMTP (Simple Mail Transfer Protocol)
電子メールをインターネット上で転送するための標準的なプロトコルです。クライアント(メールソフトなど)からメールサーバーへ、またはメールサーバー間でメールを送信する際に使用されます。
SMTP-AUTH (SMTP Authentication)
SMTPプロトコルに認証機能を追加するための拡張です。RFC 4954で定義されており、クライアントがメールサーバーに接続する際に、正当なユーザーであることを証明するためのメカニズムを提供します。これにより、不正なユーザーによるメールサーバーの利用を防ぎます。
SASL (Simple Authentication and Security Layer)
アプリケーションプロトコルに認証とデータセキュリティサービスを追加するためのフレームワークです。SMTP-AUTHはSASLを利用しており、SASLは様々な認証メカニズム(メカニズムは「チャレンジ-レスポンス」方式や「ユーザー名/パスワード」方式など、具体的な認証方法を指します)をサポートします。
CRAM-MD5 (Challenge-Response Authentication Mechanism-MD5)
SASLで利用される認証メカニズムの一つで、RFC 2195で定義されています。CRAM-MD5は、パスワードをネットワーク上で直接送信することなく認証を行う「チャレンジ-レスポンス」方式を採用しています。
- サーバーからのチャレンジ: サーバーは、クライアントに対してランダムな文字列(チャレンジ)を送信します。
- クライアントのレスポンス: クライアントは、受け取ったチャレンジ文字列と自身のパスワード(シークレット)を組み合わせてMD5ハッシュを計算します。この計算にはHMAC (Keyed-Hashing for Message Authentication) が使用されます。計算結果とユーザー名をサーバーに送信します。
- サーバーでの検証: サーバーは、自身の持つユーザーのパスワードと、自身が送信したチャレンジ文字列を使って同様のMD5ハッシュを計算し、クライアントから送られてきたレスポンスと比較します。両者が一致すれば認証成功となります。
この方式の利点は、パスワード自体がネットワーク上を流れないため、盗聴によるパスワード漏洩のリスクを低減できる点です。ただし、MD5ハッシュ関数は現在では衝突攻撃に対して脆弱であることが知られており、より強力なハッシュ関数(例: SHA-256)を使用した認証メカニズム(例: SCRAM-SHA-256)が推奨される場合もあります。しかし、2011年時点ではCRAM-MD5は広く利用されている認証メカニズムの一つでした。
HMAC (Keyed-Hashing for Message Authentication)
メッセージ認証コード (MAC) の一種で、秘密鍵とハッシュ関数(この場合はMD5)を組み合わせてメッセージの認証を行うメカニズムです。データの完全性と認証を同時に提供します。CRAM-MD5では、チャレンジ文字列と秘密鍵(パスワード)から認証レスポンスを生成するためにHMAC-MD5が使用されます。
技術的詳細
このコミットでは、net/smtp
パッケージにCRAM-MD5認証を実装するために、以下の主要な変更が加えられています。
crypto/hmac
およびfmt
パッケージのインポート: CRAM-MD5認証の計算に必要となるHMAC機能と、フォーマット済み文字列の生成に必要となるfmt
パッケージが追加でインポートされています。cramMD5Auth
構造体の定義: CRAM-MD5認証の状態を保持するための新しい構造体cramMD5Auth
が定義されています。この構造体は、認証に必要なユーザー名 (username
) とシークレット(パスワード) (secret
) を保持します。CRAMMD5Auth
コンストラクタ関数の追加:CRAMMD5Auth(username, secret string) Auth
という関数が追加され、cramMD5Auth
型のインスタンスを生成し、Auth
インターフェースとして返す役割を担います。これにより、外部からCRAM-MD5認証メカニズムを簡単に利用できるようになります。Auth
インターフェースの実装:cramMD5Auth
構造体がAuth
インターフェースのStart
メソッドとNext
メソッドを実装しています。Start
メソッドは、認証メカニズムの名前 ("CRAM-MD5"
) を返し、初期のチャレンジは不要であることを示します。Next
メソッドは、サーバーからのチャレンジ (fromServer
) を受け取り、それに基づいてレスポンスを生成します。hmac.NewMD5
を使用してHMAC-MD5ハッシュを計算し、ユーザー名と計算されたハッシュ値を結合した文字列をサーバーに返します。
- テストケースの追加:
src/pkg/net/smtp/smtp_test.go
に、CRAM-MD5認証の動作を検証するための新しいテストケースが追加されています。このテストケースは、特定のチャレンジ文字列に対して正しいレスポンスが生成されることを確認します。
コアとなるコードの変更箇所
src/pkg/net/smtp/auth.go
--- a/src/pkg/net/smtp/auth.go
+++ b/src/pkg/net/smtp/auth.go
@@ -4,7 +4,11 @@
package smtp
-import "errors"
+import (
+ "crypto/hmac"
+ "errors"
+ "fmt"
+)
// Auth is implemented by an SMTP authentication mechanism.
type Auth interface {
@@ -65,3 +69,29 @@ func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, error) {
}
return nil, nil
}
+
+type cramMD5Auth struct {
+ username, secret string
+}
+
+// CRAMMD5Auth returns an Auth that implements the CRAM-MD5 authentication
+// mechanism as defined in RFC 2195.
+// The returned Auth uses the given username and secret to authenticate
+// to the server using the challenge-response mechanism.
+func CRAMMD5Auth(username, secret string) Auth {
+ return &cramMD5Auth{username, secret}
+}
+
+func (a *cramMD5Auth) Start(server *ServerInfo) (string, []byte, error) {
+ return "CRAM-MD5", nil, nil
+}
+
+func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) {
+ if more {
+ d := hmac.NewMD5([]byte(a.secret))
+ d.Write(fromServer)
+ s := make([]byte, 0, d.Size())
+ return []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil
+ }
+ return nil, nil
+}
src/pkg/net/smtp/smtp_test.go
--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -23,6 +23,7 @@ type authTest struct {
var authTests = []authTest{
{PlainAuth("", "user", "pass", "testserver"), []string{}, "PLAIN", []string{"\x00user\x00pass"}},
{PlainAuth("foo", "bar", "baz", "testserver"), []string{}, "PLAIN", []string{"foo\x00bar\x00baz"}},
+ {CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}},
}
func TestAuth(t *testing.T) {
コアとなるコードの解説
src/pkg/net/smtp/auth.go
- インポートの追加:
crypto/hmac
とfmt
が追加されています。crypto/hmac
はHMAC-MD5ハッシュ計算のために、fmt
は最終的なレスポンス文字列のフォーマットのために必要です。 cramMD5Auth
構造体:
この構造体は、CRAM-MD5認証に必要なユーザー名とシークレット(パスワード)を保持します。type cramMD5Auth struct { username, secret string }
CRAMMD5Auth
関数:
この関数は、CRAM-MD5認証メカニズムのインスタンスを生成するためのファクトリ関数です。ユーザー名とシークレットを受け取り、func CRAMMD5Auth(username, secret string) Auth { return &cramMD5Auth{username, secret} }
Auth
インターフェースを満たすcramMD5Auth
のポインタを返します。Start
メソッド:func (a *cramMD5Auth) Start(server *ServerInfo) (string, []byte, error) { return "CRAM-MD5", nil, nil }
Auth
インターフェースのStart
メソッドの実装です。このメソッドは、サーバーに通知する認証メカニズムの名前として"CRAM-MD5"
を返します。CRAM-MD5はサーバーからのチャレンジから始まるため、クライアントが最初に送信するデータはnil
です。Next
メソッド:func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) { if more { d := hmac.NewMD5([]byte(a.secret)) d.Write(fromServer) s := make([]byte, 0, d.Size()) return []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil } return nil, nil }
Auth
インターフェースのNext
メソッドの実装です。if more
ブロックは、サーバーからチャレンジが送られてきた場合(more
がtrue
の場合)に実行されます。d := hmac.NewMD5([]byte(a.secret))
:ユーザーのシークレット(パスワード)を鍵として、MD5ハッシュ関数を使用するHMACハッシュオブジェクトを初期化します。d.Write(fromServer)
:サーバーから受け取ったチャレンジ文字列 (fromServer
) をHMACハッシュオブジェクトに書き込みます。これにより、チャレンジ文字列とシークレットを組み合わせたハッシュが計算されます。s := make([]byte, 0, d.Size())
:ハッシュ結果を格納するためのバイトスライスを準備します。return []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil
:計算されたHMAC-MD5ハッシュの16進数表現とユーザー名を結合した文字列を生成し、バイトスライスとして返します。この文字列がクライアントからサーバーへのレスポンスとなります。%x
はバイトスライスを16進数文字列としてフォーマットします。return nil, nil
:more
がfalse
の場合(つまり、認証プロセスが完了した場合)、nil
を返します。
src/pkg/net/smtp/smtp_test.go
authTests
配列への追加:
この行は、CRAM-MD5認証の新しいテストケースを追加しています。{CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}},
CRAMMD5Auth("user", "pass")
:テスト対象のCRAM-MD5認証インスタンス。[]string{"<123456.1322876914@testserver>"}
:サーバーから送られるチャレンジ文字列のシミュレーション。"CRAM-MD5"
:期待される認証メカニズム名。[]string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}
:期待されるクライアントからのレスポンス。最初の空文字列はStart
メソッドからの初期レスポンス(CRAM-MD5ではなし)を示し、2番目の文字列はチャレンジに対するレスポンスです。このレスポンスは、ユーザー名 "user" と、チャレンジ文字列 "123456.1322876914@testserver" およびパスワード "pass" から計算されたHMAC-MD5ハッシュの16進数表現 "287eb355114cf5c471c26a875f1ca4ae" を結合したものです。
このテストケースは、CRAMMD5Auth
がRFC 2195に準拠した正しいチャレンジ-レスポンスを生成できることを保証します。
関連リンク
- RFC 2195 - IMAP/POP AUTHorize Extension for Simple Challenge/Response: https://datatracker.ietf.org/doc/html/rfc2195
- RFC 4954 - SMTP Service Extension for Authentication: https://datatracker.ietf.org/doc/html/rfc4954
- Go言語
net/smtp
パッケージのドキュメント: https://pkg.go.dev/net/smtp
参考にした情報源リンク
- RFC 2195 (CRAM-MD5の定義)
- RFC 4954 (SMTP-AUTHの定義)
- Go言語の
crypto/hmac
パッケージのドキュメント - Go言語の
fmt
パッケージのドキュメント - 一般的なSMTP認証およびSASLに関する技術記事