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

[インデックス 16075] ファイルの概要

このコミットは、Go言語の標準ライブラリ net/smtp パッケージにおけるSMTP認証の挙動を修正するものです。具体的には、src/pkg/net/smtp/auth.goPLAIN 認証の条件が緩和され、src/pkg/net/smtp/smtp_test.go にその変更を検証するためのテストが追加されています。

コミット

commit ca24f9ec00c65f8e75e38ad33d1b0b3bb287e2a3
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Apr 3 10:52:20 2013 -0700

    net/smtp: allow PLAIN auth when advertised
    
    The smtp package originally allowed PLAIN whenever, but then
    the TLS check was added for paranoia, but it's too paranoid:
    it prevents using PLAIN auth even from localhost to localhost
    when the server advertises PLAIN support.
    
    This CL also permits the client to send PLAIN if the server
    advertises it.
    
    Fixes #5184
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/8279043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ca24f9ec00c65f8e75e38ad33d1b0b3bb287e2a3

元コミット内容

net/smtp: サーバーが PLAIN 認証をアドバタイズしている場合に PLAIN 認証を許可する。

smtp パッケージは元々常に PLAIN 認証を許可していましたが、その後、過度な用心のためにTLSチェックが追加されました。しかし、これは過剰であり、サーバーが PLAIN サポートをアドバタイズしている場合でも、localhostからlocalhostへの接続でさえ PLAIN 認証の使用を妨げていました。

この変更は、サーバーが PLAIN をアドバタイズしている場合、クライアントが PLAIN を送信することを許可します。

Fixes #5184

変更の背景

このコミットの背景には、Go言語の net/smtp パッケージにおける PLAIN 認証の扱いに関する問題がありました。元々、このパッケージは PLAIN 認証を常に許可していましたが、セキュリティ上の懸念から、TLS (Transport Layer Security) が確立されていない接続では PLAIN 認証を禁止するチェックが追加されました。

しかし、このTLSチェックは「過剰な用心 (too paranoid)」であることが判明しました。具体的には、SMTPサーバーが PLAIN 認証メカニズムを明示的にサポートしているとアドバタイズしている場合であっても、TLS接続が確立されていないという理由だけで PLAIN 認証が拒否されていました。これは、特にlocalhost間の通信など、TLSによる暗号化が必ずしも必要ではない、あるいはサーバー側で PLAIN 認証が意図的に許可されているシナリオにおいて、正当な認証を妨げる問題を引き起こしていました。

このコミットは、このような過剰な制限を緩和し、サーバーが PLAIN 認証をサポートしていると明示的に通知(アドバタイズ)している場合には、TLS接続がなくても PLAIN 認証を許可するように変更することで、実用性とセキュリティのバランスを取ることを目的としています。コミットメッセージにある Fixes #5184 は、Goの内部バグトラッカーにおける関連する課題番号を示していると考えられます。

前提知識の解説

SMTP (Simple Mail Transfer Protocol)

SMTPは、電子メールを送信するためのインターネット標準プロトコルです。メールクライアントからメールサーバーへ、またはメールサーバー間でメールを転送するために使用されます。

SMTP認証

SMTPサーバーは、メール送信を許可する前にユーザーの身元を確認するために認証を要求することがあります。これにより、スパム送信や不正利用を防ぎます。様々な認証メカニズムが存在します。

PLAIN認証

PLAIN 認証は、SMTP認証メカニズムの一つです。ユーザー名とパスワードを平文(クリアテキスト)で送信します。このため、盗聴されるリスクがあるため、通常はTLS/SSLなどの暗号化された接続と組み合わせて使用することが強く推奨されます。しかし、特定の環境(例: 信頼できるローカルネットワーク内、localhost間の通信)では、暗号化なしで PLAIN 認証が許可される場合があります。

TLS (Transport Layer Security)

TLSは、インターネット上で通信のセキュリティを提供する暗号化プロトコルです。以前はSSL (Secure Sockets Layer) と呼ばれていました。TLSを使用することで、通信内容が暗号化され、盗聴や改ざんから保護されます。SMTPにおいては、STARTTLS コマンドを使用して平文接続からTLS暗号化接続にアップグレードするのが一般的です。

SMTPサーバーのアドバタイズ (EHLO/HELO)

SMTPクライアントがサーバーに接続すると、まず HELO または EHLO コマンドを送信して自身を識別します。サーバーはこれに応答し、自身がサポートする機能や認証メカニズムのリストをクライアントに通知(アドバタイズ)します。例えば、サーバーが PLAIN 認証をサポートしている場合、その応答の中に AUTH PLAIN のような情報が含まれます。クライアントはこのアドバタイズされた情報に基づいて、どの認証メカニズムを使用できるかを判断します。

技術的詳細

このコミットの技術的な核心は、net/smtp パッケージの PlainAuth 構造体の Start メソッドにおける PLAIN 認証の許可条件の変更です。

変更前は、Start メソッド内で server.TLSfalse(つまり、TLS接続が確立されていない)であれば、無条件に「unencrypted connection」というエラーを返していました。これは、PLAIN 認証が平文で資格情報を送信するため、盗聴のリスクを避けるためのセキュリティ対策でした。

しかし、この厳格なチェックは、サーバーが PLAIN 認証を明示的にサポートしているとアドバタイズしている場合でも、TLS接続がないという理由だけで認証を拒否するという問題を引き起こしていました。これは、例えば開発環境でのlocalhost間の通信や、内部ネットワークでセキュリティが確保されていると見なされる環境など、TLSが必須ではない、あるいはサーバー側で意図的に PLAIN 認証が許可されているシナリオで不便でした。

このコミットでは、この挙動を修正するために、server.TLSfalse の場合でも、サーバーがアドバタイズしている認証メカニズムのリスト (server.Auth) に PLAIN が含まれているかどうかを確認するロジックが追加されました。

具体的には、以下の条件が導入されました。

  1. 接続がTLSで暗号化されていない (!server.TLS)。
  2. かつ、サーバーが PLAIN 認証をアドバタイズしていない (!advertised)。

この両方の条件が満たされた場合にのみ、「unencrypted connection」エラーが返されるようになりました。これにより、TLS接続がない場合でも、サーバーが PLAIN 認証を明示的に許可している場合には、クライアントが PLAIN 認証を使用できるようになります。

この変更は、セキュリティを完全に無視するものではなく、サーバーが PLAIN 認証を意図的に許可しているというシグナルを尊重することで、柔軟性を高めるものです。同時に、サーバーが PLAIN をアドバタイズしていないにもかかわらず、TLSなしで PLAIN 認証を試みるような、潜在的に危険なシナリオは引き続きブロックされます。

また、この変更を検証するために、smtp_test.go に新しいテストケース TestAuthPlain が追加されました。このテストは、様々な ServerInfo の設定(TLSの有無、アドバタイズされる認証メカニズム)に対して PlainAuthStart メソッドが期待通りのエラーを返すか、またはエラーを返さないかを検証します。特に、TLSなしで PLAIN がアドバタイズされている場合にエラーが発生しないことを確認するテストケースが含まれています。

コアとなるコードの変更箇所

src/pkg/net/smtp/auth.go

--- a/src/pkg/net/smtp/auth.go
+++ b/src/pkg/net/smtp/auth.go
@@ -54,7 +54,16 @@ func PlainAuth(identity, username, password, host string) Auth {
 
 func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
 	if !server.TLS {
-		return "", nil, errors.New("unencrypted connection")
+		advertised := false
+		for _, mechanism := range server.Auth {
+			if mechanism == "PLAIN" {
+				advertised = true
+				break
+			}
+		}
+		if !advertised {
+			return "", nil, errors.New("unencrypted connection")
+		}
 	}
 	if server.Name != a.host {
 		return "", nil, errors.New("wrong host name")

src/pkg/net/smtp/smtp_test.go

--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -57,6 +57,41 @@ testLoop:\n 	}\n }\n \n+func TestAuthPlain(t *testing.T) {
+\tauth := PlainAuth("foo", "bar", "baz", "servername")
+\n+\ttests := []struct {
+\t\tserver *ServerInfo
+\t\terr    string
+\t}{\n+\t\t{
+\t\t\tserver: &ServerInfo{Name: "servername", TLS: true},
+\t\t},\n+\t\t{
+\t\t\t// Okay; explicitly advertised by server.
+\t\t\tserver: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
+\t\t},\n+\t\t{
+\t\t\tserver: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
+\t\t\terr:    "unencrypted connection",
+\t\t},\n+\t\t{
+\t\t\tserver: &ServerInfo{Name: "attacker", TLS: true},
+\t\t\terr:    "wrong host name",
+\t\t},\n+\t}\n+\tfor i, tt := range tests {
+\t\t_, _, err := auth.Start(tt.server)
+\t\tgot := ""
+\t\tif err != nil {
+\t\t\tgot = err.Error()
+\t\t}\n+\t\tif got != tt.err {
+\t\t\tt.Errorf("%d. got error = %q; want %q", i, got, tt.err)
+\t\t}\n+\t}\n+}\n+\n type faker struct {\n \tio.ReadWriter\n }\n```

## コアとなるコードの解説

### `src/pkg/net/smtp/auth.go` の変更

`plainAuth` 型の `Start` メソッドは、SMTPサーバーとの認証プロセスを開始する際に呼び出されます。このメソッドは `ServerInfo` 構造体を受け取り、サーバーの現在の状態(TLS接続の有無、サポートする認証メカニズムなど)を判断します。

変更前は、以下のシンプルな条件がありました。
```go
if !server.TLS {
    return "", nil, errors.New("unencrypted connection")
}

これは、TLS接続が確立されていない場合 (server.TLSfalse)、即座に「unencrypted connection」エラーを返していました。

変更後、このブロックはより複雑になりました。

if !server.TLS { // TLS接続が確立されていない場合
    advertised := false
    for _, mechanism := range server.Auth { // サーバーがアドバタイズする認証メカニズムをループ
        if mechanism == "PLAIN" { // "PLAIN" がリストに含まれているかチェック
            advertised = true
            break // 見つかったらループを抜ける
        }
    }
    if !advertised { // "PLAIN" がアドバタイズされていない場合
        return "", nil, errors.New("unencrypted connection") // エラーを返す
    }
}

この新しいロジックは、TLS接続がない場合でも、サーバーが PLAIN 認証をサポートしていると明示的にアドバタイズしているかどうかを確認します。server.Auth は、サーバーが EHLO コマンドへの応答で通知した認証メカニズムの文字列スライスです。このスライスの中に "PLAIN" が含まれていれば advertised フラグが true に設定されます。

最終的に、TLS接続がなく、かつ PLAIN がアドバタイズされていない場合にのみ、「unencrypted connection」エラーが返されるようになりました。これにより、TLSがない場合でもサーバーが PLAIN を許可しているシナリオで認証が可能になります。

src/pkg/net/smtp/smtp_test.go の変更

TestAuthPlain という新しいテスト関数が追加されました。このテストは、PlainAuthStart メソッドが様々なサーバー情報 (ServerInfo) に対して正しく動作するかを検証します。

テストケースは以下のシナリオをカバーしています。

  1. TLS接続があり、サーバー名が一致する場合:

    {
        server: &ServerInfo{Name: "servername", TLS: true},
    },
    

    これはエラーを返すべきではありません。TLSがあるため PLAIN 認証は安全と見なされます。

  2. TLS接続はないが、サーバーが PLAIN を明示的にアドバタイズしている場合:

    {
        // Okay; explicitly advertised by server.
        server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
    },
    

    これがこのコミットの主要なテストケースです。TLSがないにもかかわらず、サーバーが PLAIN をアドバタイズしているため、エラーを返すべきではありません。

  3. TLS接続はなく、サーバーが PLAIN をアドバタイズしていない場合(他の認証メカニズムをアドバタイズしている場合):

    {
        server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
        err:    "unencrypted connection",
    },
    

    この場合、TLSがなく、PLAIN もアドバタイズされていないため、「unencrypted connection」エラーを返すべきです。

  4. サーバー名が一致しない場合:

    {
        server: &ServerInfo{Name: "attacker", TLS: true},
        err:    "wrong host name",
    },
    

    これはホスト名の不一致によるエラーをテストします。

これらのテストケースを通じて、plainAuth.Start メソッドが新しいロジックに従って期待通りの挙動を示すことが保証されます。

関連リンク

参考にした情報源リンク