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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおける Transport の挙動を修正し、HTTPSクライアントリクエスト時に tls.ConfigServerName フィールドが適切に尊重されるようにするものです。具体的には、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 のために)を指定したい場合に非常に重要です。このフィールドが正しく尊重されないと、以下のような問題が発生する可能性があります。

  1. SNIの不正確な動作: クライアントが意図したサーバー名でSNIを送信できない、またはサーバーが提供する証明書とクライアントが期待するサーバー名との間で不一致が生じる。
  2. ホスト名検証の失敗: サーバー証明書が tls.Config.ServerName に一致するものの、リクエストURLのホスト名とは異なる場合に、不必要な検証エラーが発生する。
  3. セキュリティリスク: 意図しないホスト名で検証が行われることで、中間者攻撃(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 に変更しています。ここで cfgtls.Config のインスタンスです。

変更後のロジックは以下のようになります。

  1. Transport はHTTPS接続を確立する際に、tls.Config を使用してTLSハンドシェイクを行います。
  2. ハンドシェイクが成功した後、ホスト名検証のステップに入ります。
  3. InsecureSkipVerifyfalse の場合(つまり、検証を行う場合)、conn.(*tls.Conn).VerifyHostname(cfg.ServerName) が呼び出されます。
  4. これにより、サーバーから提供された証明書が cfg.ServerName で指定されたホスト名と一致するかどうかが検証されます。もし cfg.ServerName が空文字列の場合、VerifyHostname はデフォルトの挙動として、接続先のIPアドレスまたはリクエストURLのホスト名に基づいて検証を行います。しかし、cfg.ServerName が明示的に設定されていれば、それが優先されます。

この変更により、tls.Config.ServerName が設定されている場合は、それがホスト名検証の基準として確実に使用されるようになり、開発者の意図が正しく反映されるようになりました。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/net/http/client_test.go: 新しいテストケース TestTransportUsesTLSConfigServerName が追加されました。これは、tls.Config.ServerName が正しく尊重されることを検証するためのものです。
  2. src/pkg/net/http/transport.go: TransportdialConn メソッド内で、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

このテストケースは、修正された挙動を具体的に検証します。

  1. httptest.NewTLSServer を使用して、example.com という名前の証明書を持つテスト用HTTPSサーバーを起動します。
  2. http.Transport のインスタンスを作成し、その TLSClientConfig.ServerName"example.com" に設定します。
  3. Transport.Dial フックを設定し、https://some-other-host.tld/ へのリクエストが、実際にはテストサーバーのリスナーアドレスに接続するようにします。これにより、DNS解決や実際のホスト名とは関係なく、TLSハンドシェイクとホスト名検証のロジックをテストできます。
  4. c.Get("https://some-other-host.tld/") を呼び出します。ここで重要なのは、リクエストURLのホスト名が "some-other-host.tld" であるのに対し、TLSClientConfig.ServerName"example.com" である点です。
  5. もし修正前の挙動であれば、cm.tlsHost() である "some-other-host.tld" でホスト名検証が行われ、テストサーバーの証明書(example.com)とは一致しないため、検証エラーが発生するはずです。
  6. しかし、修正後の挙動では、TLSClientConfig.ServerName である "example.com" でホスト名検証が行われるため、テストサーバーの証明書と一致し、エラーなく接続が成功します。

このテストは、tls.Config.ServerName がリクエストURLのホスト名よりも優先されるべきシナリオを正確に捉え、修正が正しく機能していることを保証します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (net/http, crypto/tls パッケージ)
  • TLS (Transport Layer Security) および SNI (Server Name Indication) に関する一般的な知識
  • コミットメッセージとコードの差分
  • Go Issue #5829 (コミットメッセージに記載されているが、直接的な検索結果は得られなかったため、コミットメッセージの内容から推測)