[インデックス 18745] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/smtp
パッケージにおける重要な修正と、それに関連するテストの追加に関するものです。具体的には、crypto/tls
パッケージの変更によって net/smtp
がTLS経由でのSMTP通信を行えなくなった問題を解決し、StartTLS
処理において tls.Config
に ServerName
を設定するように変更しています。
コミット
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/smtp
が StartTLS
を実行する際に 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接続を確立する際には、以下のいずれかを満たすことが必須となりました。
ServerName
の指定: 接続先のサーバーのホスト名を明示的に指定する。これにより、TLSハンドシェイク中にSNI拡張が送信され、サーバーは適切な証明書を提示できます。InsecureSkipVerify
のtrue
設定: サーバー証明書の検証をスキップする。これは開発やテスト目的でのみ使用されるべきであり、本番環境では推奨されません。
net/smtp
パッケージは、SMTP (Simple Mail Transfer Protocol) 通信において StartTLS
コマンドを使用して平文の接続をTLS接続にアップグレードします。この StartTLS
処理の内部で crypto/tls
を利用してTLSハンドシェイクを行いますが、以前の実装では tls.Config
に ServerName
を設定していませんでした。そのため、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.Config
を StartTLS
メソッドに渡していました。これは、StartTLS
メソッドが内部でデフォルトの tls.Config
を使用することを意図していたためです。しかし、crypto/tls
パッケージの変更により、tls.Config
に ServerName
または InsecureSkipVerify
のいずれかが設定されていないとエラーを返すようになりました。
このコミットでは、この問題を解決するために、SendMail
関数内で StartTLS
を呼び出す前に、以下のように tls.Config
オブジェクトを明示的に作成し、ServerName
を設定しています。
config := &tls.Config{ServerName: c.serverName}
ここで c.serverName
は、net/smtp
クライアントが接続しようとしているSMTPサーバーのホスト名です。このホスト名は、SendMail
関数に渡される addr
引数から解析され、Client
構造体の serverName
フィールドに格納されています。
この変更により、StartTLS
は ServerName
が設定された有効な tls.Config
オブジェクトを受け取るようになり、crypto/tls
の新しい要件を満たしてTLSハンドシェイクを正常に完了できるようになります。
また、このコミットでは、テスト容易性を向上させるために testHookStartTLS
というグローバル変数(テスト時以外は nil
)が導入されています。これは、テスト中に StartTLS
に渡される tls.Config
をフックして変更できるようにするためのものです。新しいテスト TestTLSClient
では、このフックを利用して、ローカルホストでのテスト用にカスタムのルートCA証明書を tls.Config
に追加しています。これにより、自己署名証明書を使用するテストSMTPサーバーとのTLS通信が可能になります。
新しいテスト TestTLSClient
は、以下のような構成でTLS経由のSMTP通信をエンドツーエンドでテストします。
newLocalListener
でローカルのTCPリスナーを作成し、SMTPサーバーを模倣します。- ゴルーチン内で
sendMail
関数を呼び出し、クライアントとしてSMTPサーバーに接続し、メール送信を試みます。 - メインゴルーチンでは、
ln.Accept()
でクライアントからの接続を受け入れ、serverHandle
関数でSMTPサーバーの振る舞いを模倣します。 serverHandle
内でSTARTTLS
コマンドを受信すると、自己署名証明書を使用してtls.Server
を作成し、TLS接続にアップグレードします。serverHandleTLS
関数で、TLS接続後のSMTPコマンド(EHLO, MAIL FROM, RCPT TO, DATA, QUIT)を処理します。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
の変更点
-
testHookStartTLS
グローバル変数の追加:var testHookStartTLS func(*tls.Config)
が追加されました。これはテスト目的で使用されるフック関数で、StartTLS
に渡されるtls.Config
オブジェクトをテストコードが変更できるようにします。通常運用時にはnil
です。 -
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
の変更点
-
TestTLSClient
関数の追加: この新しいテスト関数は、TLS経由でのSMTP通信のエンドツーエンドテストを行います。newLocalListener
: ローカルのTCPリスナーを作成し、テスト用のSMTPサーバーとして機能させます。sendMail
(ゴルーチン内): クライアントとしてSendMail
関数を呼び出し、テストサーバーにメールを送信します。serverHandle
: クライアントからの接続を受け入れ、SMTPサーバーの初期ハンドシェイク(EHLO, STARTTLS)を処理します。serverHandleTLS
:StartTLS
後のTLS暗号化された接続上で、残りのSMTPコマンド(MAIL FROM, RCPT TO, DATA, QUIT)を処理します。
-
smtpSender
構造体とsend
メソッド: テストサーバーがSMTP応答を送信するためのヘルパー構造体とメソッドです。 -
localhostCert
とlocalhostKey
変数の追加: テストサーバーがTLS接続を確立するために使用するPEMエンコードされた自己署名証明書と秘密鍵です。これらはnet/http/httptest
からコピーされたもので、127.0.0.1
と[::1]
のSAN IPアドレスを含んでいます。 -
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
フック関数を設定します。この関数は、SendMail
がStartTLS
を呼び出す際に実行され、クライアント側のtls.Config
にテスト用のルートCA証明書を追加します。これにより、クライアントはテストサーバーの自己署名証明書を信頼し、TLSハンドシェイクを成功させることができます。
これらの変更により、net/smtp
は crypto/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) で検索することで見つかる可能性があります。
参考にした情報源リンク
- TLS (Transport Layer Security) - Wikipedia: https://ja.wikipedia.org/wiki/Transport_Layer_Security
- Server Name Indication (SNI) - Wikipedia: https://ja.wikipedia.org/wiki/Server_Name_Indication
- Simple Mail Transfer Protocol (SMTP) - Wikipedia: https://ja.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
- Go
net/smtp
package documentation: https://pkg.go.dev/net/smtp - Go
crypto/tls
package documentation: https://pkg.go.dev/crypto/tls - Go
tls.Config
struct documentation: https://pkg.go.dev/crypto/tls#Config - Go
crypto/x509
package documentation: https://pkg.go.dev/crypto/x509 - Go
net/http/httptest
package documentation: https://pkg.go.dev/net/http/httptest (テストコードで証明書がコピーされた元) - Gerrit Code Review (Go project): https://go-review.googlesource.com/ (CL 67010043 などの変更履歴を検索する際に利用)