Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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は、パスワードをネットワーク上で直接送信することなく認証を行う「チャレンジ-レスポンス」方式を採用しています。

  1. サーバーからのチャレンジ: サーバーは、クライアントに対してランダムな文字列(チャレンジ)を送信します。
  2. クライアントのレスポンス: クライアントは、受け取ったチャレンジ文字列と自身のパスワード(シークレット)を組み合わせてMD5ハッシュを計算します。この計算にはHMAC (Keyed-Hashing for Message Authentication) が使用されます。計算結果とユーザー名をサーバーに送信します。
  3. サーバーでの検証: サーバーは、自身の持つユーザーのパスワードと、自身が送信したチャレンジ文字列を使って同様の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認証を実装するために、以下の主要な変更が加えられています。

  1. crypto/hmac および fmt パッケージのインポート: CRAM-MD5認証の計算に必要となるHMAC機能と、フォーマット済み文字列の生成に必要となるfmtパッケージが追加でインポートされています。
  2. cramMD5Auth 構造体の定義: CRAM-MD5認証の状態を保持するための新しい構造体 cramMD5Auth が定義されています。この構造体は、認証に必要なユーザー名 (username) とシークレット(パスワード) (secret) を保持します。
  3. CRAMMD5Auth コンストラクタ関数の追加: CRAMMD5Auth(username, secret string) Auth という関数が追加され、cramMD5Auth 型のインスタンスを生成し、Auth インターフェースとして返す役割を担います。これにより、外部からCRAM-MD5認証メカニズムを簡単に利用できるようになります。
  4. Auth インターフェースの実装: cramMD5Auth 構造体が Auth インターフェースの Start メソッドと Next メソッドを実装しています。
    • Start メソッドは、認証メカニズムの名前 ("CRAM-MD5") を返し、初期のチャレンジは不要であることを示します。
    • Next メソッドは、サーバーからのチャレンジ (fromServer) を受け取り、それに基づいてレスポンスを生成します。hmac.NewMD5 を使用してHMAC-MD5ハッシュを計算し、ユーザー名と計算されたハッシュ値を結合した文字列をサーバーに返します。
  5. テストケースの追加: 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/hmacfmt が追加されています。crypto/hmac はHMAC-MD5ハッシュ計算のために、fmt は最終的なレスポンス文字列のフォーマットのために必要です。
  • cramMD5Auth 構造体:
    type cramMD5Auth struct {
    	username, secret string
    }
    
    この構造体は、CRAM-MD5認証に必要なユーザー名とシークレット(パスワード)を保持します。
  • CRAMMD5Auth 関数:
    func CRAMMD5Auth(username, secret string) Auth {
    	return &cramMD5Auth{username, secret}
    }
    
    この関数は、CRAM-MD5認証メカニズムのインスタンスを生成するためのファクトリ関数です。ユーザー名とシークレットを受け取り、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 ブロックは、サーバーからチャレンジが送られてきた場合(moretrue の場合)に実行されます。
    • 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, nilmorefalse の場合(つまり、認証プロセスが完了した場合)、nil を返します。

src/pkg/net/smtp/smtp_test.go

  • authTests 配列への追加:
    {CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}},
    
    この行は、CRAM-MD5認証の新しいテストケースを追加しています。
    • 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 (CRAM-MD5の定義)
  • RFC 4954 (SMTP-AUTHの定義)
  • Go言語の crypto/hmac パッケージのドキュメント
  • Go言語の fmt パッケージのドキュメント
  • 一般的なSMTP認証およびSASLに関する技術記事