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

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

このコミットは、Go言語の標準ライブラリである net/smtp パッケージにおける重要な修正と、それに関連するテストの追加に関するものです。具体的には、crypto/tls パッケージの変更によって net/smtp がTLS経由でのSMTP通信を行えなくなった問題を解決し、StartTLS 処理において tls.ConfigServerName を設定するように変更しています。

コミット

commit a18bfb8c673591b7cbf5d16842e09e87c2c9b8cf
Author: Mike Andrews <mra@xoba.com>
Date:   Tue Mar 4 13:43:26 2014 -0800

    net/smtp: set ServerName in StartTLS, as now required by crypto/tls
    
    the crypto/tls revision d3d43f270632 (CL 67010043, requiring ServerName or InsecureSkipVerify) breaks net/smtp,
    since it seems impossible to do SMTP via TLS anymore. i've tried to fix this by simply using a tls.Config with
    ServerName, instead of a nil *tls.Config. without this fix, doing SMTP with TLS results in error "tls: either
    ServerName or InsecureSkipVerify must be specified in the tls.Config".
    
    testing: the new method TestTlsClient(...) sets up a skeletal smtp server with tls capability, and test client
    injects a "fake" certificate allowing tls to work on localhost; thus, the modification to SendMail(...) enabling
    this.
    
    Fixes #7437.
    
    LGTM=bradfitz
    R=golang-codereviews, josharian, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/70380043

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

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

元コミット内容

このコミットの目的は、net/smtp パッケージが crypto/tls パッケージの最近の変更に対応し、StartTLS ハンドシェイク時に ServerName を適切に設定することです。これにより、TLS経由でのSMTP通信が再び可能になります。

以前の crypto/tls のリビジョン d3d43f270632 (CL 67010043) では、tls.Config オブジェクトにおいて ServerName または InsecureSkipVerify のいずれかを明示的に指定することが必須となりました。この変更により、net/smtpStartTLS を実行する際に nil*tls.Config を渡していたため、「tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config」というエラーが発生し、TLSを使用したSMTP通信が不可能になっていました。

このコミットでは、StartTLS 呼び出し時に nil ではなく、ServerName が設定された tls.Config オブジェクトを渡すことでこの問題を修正しています。また、この変更を検証するために、TLS機能を備えたスケルトンSMTPサーバーと、ローカルホストでのTLSテストを可能にする「偽の」証明書を注入するテストクライアントを含む新しいテストメソッド TestTlsClient が追加されています。

変更の背景

この変更の背景には、Go言語の crypto/tls パッケージにおけるセキュリティ強化があります。TLS (Transport Layer Security) は、インターネット上での安全な通信を確立するためのプロトコルであり、その中でもサーバー証明書の検証は非常に重要です。

以前の crypto/tls の実装では、TLSクライアントがサーバーに接続する際に、サーバーのホスト名を指定しない場合でも接続が確立されることがありました。しかし、これはセキュリティ上のリスクをはらんでいます。特に、SNI (Server Name Indication) を使用する仮想ホスティング環境では、クライアントがどのサーバーに接続しようとしているかを明示しないと、正しい証明書が提示されず、中間者攻撃 (Man-in-the-Middle attack) のリスクが高まります。

crypto/tls のリビジョン d3d43f270632 (CL 67010043) は、このセキュリティリスクに対処するために導入されました。この変更により、tls.Config を使用してTLS接続を確立する際には、以下のいずれかを満たすことが必須となりました。

  1. ServerName の指定: 接続先のサーバーのホスト名を明示的に指定する。これにより、TLSハンドシェイク中にSNI拡張が送信され、サーバーは適切な証明書を提示できます。
  2. InsecureSkipVerifytrue 設定: サーバー証明書の検証をスキップする。これは開発やテスト目的でのみ使用されるべきであり、本番環境では推奨されません。

net/smtp パッケージは、SMTP (Simple Mail Transfer Protocol) 通信において StartTLS コマンドを使用して平文の接続をTLS接続にアップグレードします。この StartTLS 処理の内部で crypto/tls を利用してTLSハンドシェイクを行いますが、以前の実装では tls.ConfigServerName を設定していませんでした。そのため、crypto/tls の変更が導入された結果、net/smtp がTLS接続を確立しようとすると、新しい要件を満たせずエラーが発生するようになりました。

このコミットは、この互換性の問題を解決し、net/smtp が最新の crypto/tls のセキュリティ要件に準拠して動作するようにするためのものです。

前提知識の解説

TLS (Transport Layer Security)

TLSは、インターネット上でデータを安全にやり取りするための暗号化プロトコルです。以前はSSL (Secure Sockets Layer) と呼ばれていました。TLSは、クライアントとサーバー間の通信を暗号化し、データの盗聴や改ざんを防ぎ、通信相手の認証を行うことで、通信の機密性、完全性、認証性を提供します。

TLSハンドシェイクは、クライアントとサーバーが安全な通信を開始する前に行われる一連のステップです。このハンドシェイク中に、両者はプロトコルのバージョン、暗号スイート、セッションキーなどをネゴシエートし、サーバーは自身の身元を証明するためにデジタル証明書を提示します。

サーバー証明書と認証局 (CA)

サーバー証明書は、サーバーの公開鍵と、その公開鍵が特定のドメイン名に属していることを証明するデジタル署名が含まれたファイルです。このデジタル署名は、信頼できる第三者機関である認証局 (CA) によって発行されます。クライアントは、CAの公開鍵を使用してサーバー証明書の署名を検証し、サーバーが主張する身元が正当であることを確認します。

SNI (Server Name Indication)

SNIは、TLSプロトコルの拡張機能の一つです。一つのIPアドレスで複数のウェブサイト(ドメイン名)をホストしているサーバーにおいて、クライアントがTLSハンドシェイクの初期段階で接続したいホスト名をサーバーに伝えるために使用されます。これにより、サーバーはクライアントが要求しているホスト名に対応する適切なサーバー証明書を提示することができます。SNIがない場合、サーバーはどの証明書を提示すべきか判断できず、デフォルトの証明書を提示するか、接続を拒否する可能性があります。

net/smtp パッケージ

net/smtp は、Go言語の標準ライブラリで、SMTP (Simple Mail Transfer Protocol) クライアントを実装するためのパッケージです。SMTPは、電子メールを送信するためのプロトコルです。net/smtp パッケージは、メールサーバーへの接続、認証、メールの送信などの機能を提供します。

StartTLS コマンド

StartTLS は、SMTPプロトコルにおけるコマンドの一つです。これは、平文で確立されたSMTP接続を、TLS暗号化された接続にアップグレードするために使用されます。クライアントが StartTLS コマンドをサーバーに送信すると、サーバーはTLSハンドシェイクを開始する準備ができたことを応答し、その後、通常のTLSハンドシェイクプロセスが開始されます。ハンドシェイクが成功すると、それ以降のSMTP通信は暗号化されます。

crypto/tls パッケージ

crypto/tls は、Go言語の標準ライブラリで、TLSプロトコルを実装するためのパッケージです。TLSクライアントおよびサーバーの機能を提供し、証明書の管理、ハンドシェイクの実行、暗号化されたデータの送受信などを行います。

tls.Config 構造体

tls.Config は、crypto/tls パッケージで使用される構造体で、TLS接続の様々な設定オプションをカプセル化します。これには、証明書、キー、ルートCA、プロトコルバージョン、暗号スイート、そしてこのコミットで重要となる ServerName などの設定が含まれます。

  • ServerName: クライアントが接続しようとしているサーバーのホスト名を指定します。これはSNI拡張で使用されます。
  • InsecureSkipVerify: true に設定すると、サーバー証明書の検証をスキップします。これはセキュリティリスクがあるため、本番環境での使用は避けるべきです。

技術的詳細

このコミットの技術的な核心は、net/smtp パッケージの SendMail 関数内で StartTLS を呼び出す際に、tls.Config オブジェクトに ServerName フィールドを適切に設定することです。

以前の net/smtp の実装では、c.StartTLS(nil) のように nil*tls.ConfigStartTLS メソッドに渡していました。これは、StartTLS メソッドが内部でデフォルトの tls.Config を使用することを意図していたためです。しかし、crypto/tls パッケージの変更により、tls.ConfigServerName または InsecureSkipVerify のいずれかが設定されていないとエラーを返すようになりました。

このコミットでは、この問題を解決するために、SendMail 関数内で StartTLS を呼び出す前に、以下のように tls.Config オブジェクトを明示的に作成し、ServerName を設定しています。

config := &tls.Config{ServerName: c.serverName}

ここで c.serverName は、net/smtp クライアントが接続しようとしているSMTPサーバーのホスト名です。このホスト名は、SendMail 関数に渡される addr 引数から解析され、Client 構造体の serverName フィールドに格納されています。

この変更により、StartTLSServerName が設定された有効な tls.Config オブジェクトを受け取るようになり、crypto/tls の新しい要件を満たしてTLSハンドシェイクを正常に完了できるようになります。

また、このコミットでは、テスト容易性を向上させるために testHookStartTLS というグローバル変数(テスト時以外は nil)が導入されています。これは、テスト中に StartTLS に渡される tls.Config をフックして変更できるようにするためのものです。新しいテスト TestTLSClient では、このフックを利用して、ローカルホストでのテスト用にカスタムのルートCA証明書を tls.Config に追加しています。これにより、自己署名証明書を使用するテストSMTPサーバーとのTLS通信が可能になります。

新しいテスト TestTLSClient は、以下のような構成でTLS経由のSMTP通信をエンドツーエンドでテストします。

  1. newLocalListener でローカルのTCPリスナーを作成し、SMTPサーバーを模倣します。
  2. ゴルーチン内で sendMail 関数を呼び出し、クライアントとしてSMTPサーバーに接続し、メール送信を試みます。
  3. メインゴルーチンでは、ln.Accept() でクライアントからの接続を受け入れ、serverHandle 関数でSMTPサーバーの振る舞いを模倣します。
  4. serverHandle 内で STARTTLS コマンドを受信すると、自己署名証明書を使用して tls.Server を作成し、TLS接続にアップグレードします。
  5. serverHandleTLS 関数で、TLS接続後のSMTPコマンド(EHLO, MAIL FROM, RCPT TO, DATA, QUIT)を処理します。
  6. init 関数内で testHookStartTLS を設定し、クライアント側の tls.Config にテスト用のルートCA証明書を追加することで、自己署名証明書を持つサーバーとのTLSハンドシェイクを成功させます。

このテストは、net/smtp クライアントが StartTLS を使用してTLS接続を確立し、その上でSMTPコマンドを正しく実行できることを保証します。

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

src/pkg/net/smtp/smtp.go

--- a/src/pkg/net/smtp/smtp.go
+++ b/src/pkg/net/smtp/smtp.go
@@ -264,6 +264,8 @@ func (c *Client) Data() (io.WriteCloser, error) {
 	return &dataCloser{c, c.Text.DotWriter()}, nil
 }
 
+var testHookStartTLS func(*tls.Config) // nil, except for tests
+
 // SendMail connects to the server at addr, switches to TLS if
 // possible, authenticates with the optional mechanism a if possible,
 // and then sends an email from address from, to addresses to, with
@@ -278,7 +280,11 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
 		return err
 	}
 	if ok, _ := c.Extension("STARTTLS"); ok {
-		if err = c.StartTLS(nil); err != nil {
+		config := &tls.Config{ServerName: c.serverName}
+		if testHookStartTLS != nil {
+			testHookStartTLS(config)
+		}
+		if err = c.StartTLS(config); err != nil {
 			return err
 		}
 	}

src/pkg/net/smtp/smtp_test.go

--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -7,6 +7,8 @@ package smtp
 import (
 	"bufio"
 	"bytes"
+	"crypto/tls"
+	"crypto/x509"
 	"io"
 	"net"
 	"net/textproto"
@@ -548,3 +550,145 @@ AUTH PLAIN AHVzZXIAcGFzcw==
 *
 QUIT
 `
+
+func TestTLSClient(t *testing.T) {
+	ln := newLocalListener(t)
+	defer ln.Close()
+	errc := make(chan error)
+	go func() {
+		errc <- sendMail(ln.Addr().String())
+	}()
+	conn, err := ln.Accept()
+	if err != nil {
+		t.Fatalf("failed to accept connection: %v", err)
+	}
+	defer conn.Close()
+	if err := serverHandle(conn, t); err != nil {
+		t.Fatalf("failed to handle connection: %v", err)
+	}
+	if err := <-errc; err != nil {
+		t.Fatalf("client error: %v", err)
+}
+
+func newLocalListener(t *testing.T) net.Listener {
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		ln, err = net.Listen("tcp6", "[::1]:0")
+	}
+	if err != nil {
+		t.Fatal(err)
+	}
+	return ln
+}
+
+type smtpSender struct {
+	w io.Writer
+}
+
+func (s smtpSender) send(f string) {
+	s.w.Write([]byte(f + "\r\n"))
+}
+
+// smtp server, finely tailored to deal with our own client only!
+func serverHandle(c net.Conn, t *testing.T) error {
+	send := smtpSender{c}.send
+	send("220 127.0.0.1 ESMTP service ready")
+	s := bufio.NewScanner(c)
+	for s.Scan() {
+		switch s.Text() {
+		case "EHLO localhost":
+			send("250-127.0.0.1 ESMTP offers a warm hug of welcome")
+			send("250-STARTTLS")
+			send("250 Ok")
+		case "STARTTLS":
+			send("220 Go ahead")
+			keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
+			if err != nil {
+				return err
+			}
+			config := &tls.Config{Certificates: []tls.Certificate{keypair}}
+			c = tls.Server(c, config)
+			defer c.Close()
+			return serverHandleTLS(c, t)
+		default:
+			t.Fatalf("unrecognized command: %q", s.Text())
+		}
+	}
+	return s.Err()
+}
+
+func serverHandleTLS(c net.Conn, t *testing.T) error {
+	send := smtpSender{c}.send
+	s := bufio.NewScanner(c)
+	for s.Scan() {
+		switch s.Text() {
+		case "EHLO localhost":
+			send("250 Ok")
+		case "MAIL FROM:<joe1@example.com>":
+			send("250 Ok")
+		case "RCPT TO:<joe2@example.com>":
+			send("250 Ok")
+		case "DATA":
+			send("354 send the mail data, end with .")
+			send("250 Ok")
+		case "Subject: test":
+		case "":
+		case "howdy!":
+		case ".":
+		case "QUIT":
+			send("221 127.0.0.1 Service closing transmission channel")
+			return nil
+		default:
+			t.Fatalf("unrecognized command during TLS: %q", s.Text())
+		}
+	}
+	return s.Err()
+}
+
+func init() {
+	testRootCAs := x509.NewCertPool()
+	testRootCAs.AppendCertsFromPEM(localhostCert)
+	testHookStartTLS = func(config *tls.Config) {
+		config.RootCAs = testRootCAs
+	}
+}
+
+func sendMail(hostPort string) error {
+	host, _, err := net.SplitHostPort(hostPort)
+	if err != nil {
+		return err
+	}
+	auth := PlainAuth("", "", "", host)
+	from := "joe1@example.com"
+	to := []string{"joe2@example.com"}
+	return SendMail(hostPort, auth, from, to, []byte("Subject: test\n\nhowdy!"))
+}
+
+// (copied from net/http/httptest)
+// localhostCert is a PEM-encoded TLS cert with SAN IPs
+// "127.0.0.1" and "[::1]", expiring at the last second of 2049 (the end
+// of ASN.1 time).
+// generated from src/pkg/crypto/tls:
+// go run generate_cert.go  --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
+var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
+MIIBdzCCASOgAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD
+bzAeFw03MDAxMDEwMDAwMDBaFw00OTEyMzEyMzU5NTlaMBIxEDAOBgNVBAoTB0Fj
+bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAN55NcYKZeInyTuhcCwFMhDHCmwa
+IUSdtXdcbItRB/yfXGBhiex00IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEA
+AaNoMGYwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
+EwEB/wQFMAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAA
+AAAAAAAAAAAAAAEwCwYJKoZIhvcNAQEFA0EAAoQn/ytgqpiLcZu9XKbCJsJcvkgk
+Se6AbGXgSlq+ZCEVo0qIwSgeBqmsJxUu7NCSOwVJLYNEBO2DtIxoYVk+MA==
+-----END CERTIFICATE-----`)
+
+// localhostKey is the private key for localhostCert.
+var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
+MIIBPAIBAAJBAN55NcYKZeInyTuhcCwFMhDHCmwaIUSdtXdcbItRB/yfXGBhiex0
+0IaLXQnSU+QZWYqeTEbFSgihqi1PUDy8CAwEAAQJBAQdUx66rfh8sYsgfdcvV
+NoafYpnEcB5s4m/vSVe6SU7dCK6eYec9f9wpT353ljhDUHq3EbmE4foNzJngh35d
+AekCIQDhRQG5Li0Wj8TM4obOnnXUXf1jRv0UkzE9AHWLG5q3AwIhAPzSjpYUDjVW
+MCUXgckTpKCuGwbJk7424Nb8bLzf3kllAiA5mUBgjfr/WtFSJdWcPQ4Zt9KTMNKD
+EUO0ukpTwEIl6wIhAMbGqZK3zAAFdq8DD2jPx+UJXnh0rnOkZBzDtJ6/iN69AiEA
+1Aq8MJgTaYsDQWyU/hDq5YkDJc9e9DSCvUIzqxQWMQE=
+-----END RSA PRIVATE KEY-----`)

コアとなるコードの解説

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

  1. testHookStartTLS グローバル変数の追加: var testHookStartTLS func(*tls.Config) が追加されました。これはテスト目的で使用されるフック関数で、StartTLS に渡される tls.Config オブジェクトをテストコードが変更できるようにします。通常運用時には nil です。

  2. SendMail 関数内の StartTLS 呼び出しの変更: 以前は c.StartTLS(nil) と呼ばれていましたが、以下のように変更されました。

    config := &tls.Config{ServerName: c.serverName}
    if testHookStartTLS != nil {
        testHookStartTLS(config)
    }
    if err = c.StartTLS(config); err != nil {
        return err
    }
    
    • tls.Config{ServerName: c.serverName}: tls.Config オブジェクトが明示的に作成され、ServerName フィールドにクライアントが接続しようとしているサーバーのホスト名 (c.serverName) が設定されます。これにより、crypto/tls の新しい要件が満たされます。
    • if testHookStartTLS != nil { testHookStartTLS(config) }: テストフックが設定されている場合、そのフック関数が呼び出され、tls.Config オブジェクトをさらにカスタマイズできるようになります。これは主にテストで自己署名証明書を信頼するために使用されます。

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

  1. TestTLSClient 関数の追加: この新しいテスト関数は、TLS経由でのSMTP通信のエンドツーエンドテストを行います。

    • newLocalListener: ローカルのTCPリスナーを作成し、テスト用のSMTPサーバーとして機能させます。
    • sendMail (ゴルーチン内): クライアントとして SendMail 関数を呼び出し、テストサーバーにメールを送信します。
    • serverHandle: クライアントからの接続を受け入れ、SMTPサーバーの初期ハンドシェイク(EHLO, STARTTLS)を処理します。
    • serverHandleTLS: StartTLS 後のTLS暗号化された接続上で、残りのSMTPコマンド(MAIL FROM, RCPT TO, DATA, QUIT)を処理します。
  2. smtpSender 構造体と send メソッド: テストサーバーがSMTP応答を送信するためのヘルパー構造体とメソッドです。

  3. localhostCertlocalhostKey 変数の追加: テストサーバーがTLS接続を確立するために使用するPEMエンコードされた自己署名証明書と秘密鍵です。これらは net/http/httptest からコピーされたもので、127.0.0.1[::1] のSAN IPアドレスを含んでいます。

  4. init 関数の追加: この init 関数は、パッケージがロードされる際に自動的に実行されます。

    func init() {
        testRootCAs := x509.NewCertPool()
        testRootCAs.AppendCertsFromPEM(localhostCert)
        testHookStartTLS = func(config *tls.Config) {
            config.RootCAs = testRootCAs
        }
    }
    
    • x509.NewCertPool(): 新しい証明書プールを作成します。
    • testRootCAs.AppendCertsFromPEM(localhostCert): テスト用の自己署名証明書 (localhostCert) を信頼できるルートCAとして証明書プールに追加します。
    • testHookStartTLS = func(config *tls.Config) { config.RootCAs = testRootCAs }: testHookStartTLS フック関数を設定します。この関数は、SendMailStartTLS を呼び出す際に実行され、クライアント側の tls.Config にテスト用のルートCA証明書を追加します。これにより、クライアントはテストサーバーの自己署名証明書を信頼し、TLSハンドシェイクを成功させることができます。

これらの変更により、net/smtpcrypto/tls の新しいセキュリティ要件に準拠し、TLS経由でのSMTP通信が再び可能になりました。また、堅牢なテストケースが追加されたことで、将来的な回帰を防ぐための基盤が強化されました。

関連リンク

  • Go issue #7437: https://github.com/golang/go/issues/7437
  • Go CL 70380043: https://golang.org/cl/70380043
  • crypto/tls revision d3d43f270632 (CL 67010043): このコミットの直接の原因となった crypto/tls の変更に関する情報。具体的なリンクはコミットメッセージにはありませんが、Goのコードレビューシステム (Gerrit) で検索することで見つかる可能性があります。

参考にした情報源リンク