[インデックス 16849] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおける Transport
の挙動を修正し、HTTPSクライアントリクエスト時に tls.Config
の ServerName
フィールドが適切に尊重されるようにするものです。具体的には、TLSハンドシェイク後のホスト名検証において、tls.Config.ServerName
が設定されている場合にそれが優先的に使用されるように変更されました。
コミット
- コミットハッシュ:
baa9ca032bf257eb931f3fe82897650e21206093
- Author: Brad Fitzpatrick bradfitz@golang.org
- Date: Mon Jul 22 22:39:09 2013 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/baa9ca032bf257eb931f3fe82897650e21206093
元コミット内容
net/http: respect tls.Config.ServerName in Transport
When making an HTTPS client request, respect the
ServerName field in the tls.Config.
Fixes #5829
R=golang-dev, agl, adg
CC=golang-dev
https://golang.org/cl/11691043
変更の背景
この変更は、Goの net/http
パッケージがHTTPSクライアントリクエストを行う際に、tls.Config
構造体内の ServerName
フィールドを正しく扱っていなかった問題(Issue #5829)を修正するために行われました。
従来の挙動では、http.Transport
がHTTPS接続を確立する際、TLSハンドシェイク後のサーバー証明書のホスト名検証において、tls.Config.ServerName
に値が設定されていても、それが適切に利用されない場合がありました。特に、リクエストURLのホスト名が tls.Config.ServerName
と異なる場合に、ServerName
が無視され、意図しないホスト名で検証が行われる可能性がありました。
tls.Config.ServerName
は、クライアントが特定のサーバー名(例えば、SNI: Server Name Indication のために)を指定したい場合に非常に重要です。このフィールドが正しく尊重されないと、以下のような問題が発生する可能性があります。
- SNIの不正確な動作: クライアントが意図したサーバー名でSNIを送信できない、またはサーバーが提供する証明書とクライアントが期待するサーバー名との間で不一致が生じる。
- ホスト名検証の失敗: サーバー証明書が
tls.Config.ServerName
に一致するものの、リクエストURLのホスト名とは異なる場合に、不必要な検証エラーが発生する。 - セキュリティリスク: 意図しないホスト名で検証が行われることで、中間者攻撃(Man-in-the-Middle attack)に対する脆弱性が生じる可能性。
このコミットは、これらの問題を解決し、開発者が tls.Config.ServerName
を用いてHTTPSクライアントの挙動をより正確に制御できるようにすることを目的としています。
前提知識の解説
1. net/http
パッケージと http.Transport
Go言語の net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。
http.Client
はHTTPリクエストを行うための高レベルなインターフェースを提供し、その内部で http.Transport
を使用して実際のネットワーク通信を行います。
http.Transport
は、HTTPリクエストのラウンドトリップ(接続の確立、リクエストの送信、レスポンスの受信、接続の再利用など)を処理する役割を担います。HTTPS通信の場合、http.Transport
は内部で crypto/tls
パッケージを利用してTLSハンドシェイクや証明書検証を行います。
2. crypto/tls
パッケージと tls.Config
crypto/tls
パッケージは、TLS (Transport Layer Security) プロトコルを実装しており、セキュアな通信を提供します。
tls.Config
構造体は、TLS接続の設定を定義するために使用されます。これには、証明書、キー、ルートCA証明書プール、プロトコルバージョン、暗号スイートなど、TLSハンドシェイクの挙動を制御するための様々なフィールドが含まれます。
3. tls.Config.ServerName
tls.Config.ServerName
フィールドは、TLSクライアントがサーバーに接続する際に、サーバー証明書の検証に使用するホスト名を指定するために使用されます。また、Server Name Indication (SNI) 拡張を介してサーバーに送信されるサーバー名としても機能します。
- Server Name Indication (SNI): TLSプロトコルの拡張機能で、クライアントがTLSハンドシェイクの開始時に接続しようとしているホスト名をサーバーに伝えることができます。これにより、単一のIPアドレスで複数のドメインのHTTPSサービスをホストしているサーバーが、適切な証明書をクライアントに提供できるようになります。
- ホスト名検証: TLSハンドシェイクが完了した後、クライアントはサーバーから提供された証明書が、接続しようとしているホスト名と一致するかどうかを検証します。これは、中間者攻撃を防ぐための重要なセキュリティメカニズムです。
tls.Config.ServerName
が設定されている場合、この値が証明書のホスト名検証に使用されるべきです。
4. InsecureSkipVerify
tls.Config.InsecureSkipVerify
フィールドは、サーバー証明書の検証をスキップするかどうかを制御するブール値です。true
に設定すると、証明書の検証は行われず、セキュリティリスクが高まります。通常、本番環境では false
に設定し、厳密な検証を行うべきです。
技術的詳細
このコミットの技術的な核心は、net/http/transport.go
内の dialConn
メソッドにおけるTLSホスト名検証ロジックの変更にあります。
変更前は、conn.(*tls.Conn).VerifyHostname()
メソッドの引数として cm.tlsHost()
が渡されていました。cm.tlsHost()
は、HTTPリクエストのURLから抽出されたホスト名(または Host
ヘッダーの値)を基に決定されるホスト名です。
問題は、tls.Config.ServerName
が設定されているにもかかわらず、cm.tlsHost()
が優先されてしまうケースがあったことです。tls.Config.ServerName
は、開発者が明示的に「このホスト名で証明書を検証してほしい」と指定する意図を持つため、これが無視されるのは誤った挙動でした。特に、リクエストURLのホスト名と tls.Config.ServerName
が異なるが、tls.Config.ServerName
で指定されたホスト名で証明書が発行されているようなシナリオで問題が発生しました。
このコミットでは、VerifyHostname
の引数を cm.tlsHost()
から cfg.ServerName
に変更しています。ここで cfg
は tls.Config
のインスタンスです。
変更後のロジックは以下のようになります。
Transport
はHTTPS接続を確立する際に、tls.Config
を使用してTLSハンドシェイクを行います。- ハンドシェイクが成功した後、ホスト名検証のステップに入ります。
InsecureSkipVerify
がfalse
の場合(つまり、検証を行う場合)、conn.(*tls.Conn).VerifyHostname(cfg.ServerName)
が呼び出されます。- これにより、サーバーから提供された証明書が
cfg.ServerName
で指定されたホスト名と一致するかどうかが検証されます。もしcfg.ServerName
が空文字列の場合、VerifyHostname
はデフォルトの挙動として、接続先のIPアドレスまたはリクエストURLのホスト名に基づいて検証を行います。しかし、cfg.ServerName
が明示的に設定されていれば、それが優先されます。
この変更により、tls.Config.ServerName
が設定されている場合は、それがホスト名検証の基準として確実に使用されるようになり、開発者の意図が正しく反映されるようになりました。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
src/pkg/net/http/client_test.go
: 新しいテストケースTestTransportUsesTLSConfigServerName
が追加されました。これは、tls.Config.ServerName
が正しく尊重されることを検証するためのものです。src/pkg/net/http/transport.go
:Transport
のdialConn
メソッド内で、TLSホスト名検証のロジックが修正されました。
src/pkg/net/http/transport.go
の変更点
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -514,8 +514,8 @@ func (t *Transport) dialConn(cm *connectMethod) (*persistConn, error) {
if err = conn.(*tls.Conn).Handshake(); err != nil {
return nil, err
}
- if t.TLSClientConfig == nil || !t.TLSClientConfig.InsecureSkipVerify {
- if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil {
+ if !cfg.InsecureSkipVerify {
+ if err = conn.(*tls.Conn).VerifyHostname(cfg.ServerName); err != nil {
return nil, err
t.TLSClientConfig == nil || !t.TLSClientConfig.InsecureSkipVerify {
if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil {
src/pkg/net/http/client_test.go
の追加テスト
--- a/src/pkg/net/http/client_test.go
+++ b/src/pkg/net/http/client_test.go
@@ -666,6 +666,36 @@ func TestClientWithIncorrectTLSServerName(t *testing.T) {
}
}
+// Test for golang.org/issue/5829; the Transport should respect TLSClientConfig.ServerName
+// when not empty.
+//
+// tls.Config.ServerName (non-empty, set to "example.com") takes
+// precedence over "some-other-host.tld" which previously incorrectly
+// took precedence. We don't actually connect to (or even resolve)
+// "some-other-host.tld", though, because of the Transport.Dial hook.
+//
+// The httptest.Server has a cert with "example.com" as its name.
+func TestTransportUsesTLSConfigServerName(t *testing.T) {
+ defer afterTest(t)
+ ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ w.Write([]byte("Hello"))
+ }))
+ defer ts.Close()
+
+ tr := newTLSTransport(t, ts)
+ tr.TLSClientConfig.ServerName = "example.com" // one of httptest's Server cert names
+ tr.Dial = func(netw, addr string) (net.Conn, error) {
+ return net.Dial(netw, ts.Listener.Addr().String())
+ }
+ defer tr.CloseIdleConnections()
+ c := &Client{Transport: tr}
+ res, err := c.Get("https://some-other-host.tld/")
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+}
+
// Verify Response.ContentLength is populated. http://golang.org/issue/4126
func TestClientHeadContentLength(t *testing.T) {
defer afterTest(t)
コアとなるコードの解説
src/pkg/net/http/transport.go
の変更
変更前のコード:
if t.TLSClientConfig == nil || !t.TLSClientConfig.InsecureSkipVerify {
if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil {
return nil, err
}
}
変更後のコード:
if !cfg.InsecureSkipVerify {
if err = conn.(*tls.Conn).VerifyHostname(cfg.ServerName); err != nil {
return nil, err
}
}
この変更は非常に重要です。
t.TLSClientConfig == nil
のチェックが削除されました。これは、cfg
変数が既にtls.Config
のインスタンスを指しているため、不要になったためです。- 最も重要な変更は、
VerifyHostname
メソッドに渡される引数がcm.tlsHost()
からcfg.ServerName
に変更された点です。cm.tlsHost()
は、HTTPリクエストのターゲットホスト名(例:c.Get("https://some-other-host.tld/")
のsome-other-host.tld
)から導出されるホスト名です。cfg.ServerName
は、tls.Config
に明示的に設定されたサーバー名です。
この修正により、tls.Config.ServerName
が設定されている場合、TLSハンドシェイク後のホスト名検証において、この明示的に指定されたサーバー名が優先的に使用されるようになります。これにより、開発者が意図した通りのホスト名検証が行われ、SNIの挙動とも整合性が取れるようになります。
src/pkg/net/http/client_test.go
の追加テスト TestTransportUsesTLSConfigServerName
このテストケースは、修正された挙動を具体的に検証します。
httptest.NewTLSServer
を使用して、example.com
という名前の証明書を持つテスト用HTTPSサーバーを起動します。http.Transport
のインスタンスを作成し、そのTLSClientConfig.ServerName
を"example.com"
に設定します。Transport.Dial
フックを設定し、https://some-other-host.tld/
へのリクエストが、実際にはテストサーバーのリスナーアドレスに接続するようにします。これにより、DNS解決や実際のホスト名とは関係なく、TLSハンドシェイクとホスト名検証のロジックをテストできます。c.Get("https://some-other-host.tld/")
を呼び出します。ここで重要なのは、リクエストURLのホスト名が"some-other-host.tld"
であるのに対し、TLSClientConfig.ServerName
は"example.com"
である点です。- もし修正前の挙動であれば、
cm.tlsHost()
である"some-other-host.tld"
でホスト名検証が行われ、テストサーバーの証明書(example.com
)とは一致しないため、検証エラーが発生するはずです。 - しかし、修正後の挙動では、
TLSClientConfig.ServerName
である"example.com"
でホスト名検証が行われるため、テストサーバーの証明書と一致し、エラーなく接続が成功します。
このテストは、tls.Config.ServerName
がリクエストURLのホスト名よりも優先されるべきシナリオを正確に捉え、修正が正しく機能していることを保証します。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/baa9ca032bf257eb931f3fe82897650e21206093
- Go CL (Code Review): https://golang.org/cl/11691043
参考にした情報源リンク
- Go言語の公式ドキュメント (
net/http
,crypto/tls
パッケージ) - TLS (Transport Layer Security) および SNI (Server Name Indication) に関する一般的な知識
- コミットメッセージとコードの差分
- Go Issue #5829 (コミットメッセージに記載されているが、直接的な検索結果は得られなかったため、コミットメッセージの内容から推測)