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

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

このコミットでは、Go言語の標準ライブラリである net/http パッケージにおけるTLS (Transport Layer Security) クライアントの挙動が改善されています。具体的には、HTTPリクエストごとに TLSClientConfig.ServerName が自動的に設定されるようになり、SNI (Server Name Indication) が http.Client を利用するユーザーにとって「ただ動作する」ように変更されました。

変更されたファイルは以下の通りです。

  • src/pkg/net/http/client_test.go: net/http クライアントのテストファイル。SNIの自動設定を検証するための新しいテストケースが追加されました。
  • src/pkg/net/http/httptest/server.go: テスト用のHTTPサーバーを提供する httptest パッケージのファイル。テストサーバーが使用する証明書がCA (Certificate Authority) 証明書として機能するように変更されました。
  • src/pkg/net/http/transport.go: net/http クライアントのトランスポート層の実装ファイル。TLS接続確立時に ServerName を自動設定するロジックが追加されました。

コミット

commit 2eb6a16e16411a394527447b4f6ec0ba838b18e8
Author: Dave Borowitz <dborowitz@google.com>
Date:   Wed Aug 22 09:15:41 2012 -0700

    net/http: Set TLSClientConfig.ServerName on every HTTP request.
    
    This makes SNI "just work" for callers using the standard http.Client.
    
    Since we now have a test that depends on the httptest.Server cert, change
    the cert to be a CA (keeping all other fields the same).
    
    R=bradfitz
    CC=agl, dsymonds, gobot, golang-dev
    https://golang.org/cl/6448154

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

https://github.com/golang/go/commit/2eb6a16e16411a394527447b4f6ec0ba838b18e8

元コミット内容

net/http: Set TLSClientConfig.ServerName on every HTTP request.

This makes SNI "just work" for callers using the standard http.Client.

Since we now have a test that depends on the httptest.Server cert, change
the cert to be a CA (keeping all other fields the same).

R=bradfitz
CC=agl, dsymonds, gobot, golang-dev
https://golang.org/cl/6448154

変更の背景

この変更の主な背景は、TLS (Transport Layer Security) の拡張である SNI (Server Name Indication) を、Goの標準HTTPクライアント (http.Client) でより透過的に、かつ自動的に機能させることにあります。

SNIは、単一のIPアドレスとポート番号で複数のTLS証明書をホストするサーバーにおいて、クライアントがどのホスト名に接続しようとしているかをサーバーに伝えるための仕組みです。これにより、サーバーは適切な証明書をクライアントに提示し、TLSハンドシェイクを正常に完了させることができます。

このコミット以前は、Goの http.Client を使用してSNIが必要なサーバーに接続する場合、開発者が明示的に http.TransportTLSClientConfig 内の ServerName フィールドを設定する必要がありました。これは、特に多くの異なるホストに接続するアプリケーションにとって、煩雑な作業であり、SNIの概念を理解していない開発者にとっては予期せぬTLS接続エラーの原因となる可能性がありました。

このコミットは、http.Client がリクエスト先のホスト名から自動的に ServerName を推測し、TLS設定に適用することで、この手動設定の必要性を排除し、SNIを「ただ動作する」ようにすることを目指しています。これにより、開発者はSNIの詳細を意識することなく、安全なHTTPS接続を確立できるようになります。

また、この変更に伴い、SNIの自動設定が正しく機能することを検証するためのテストが追加されました。これらのテストをサポートするために、httptest.Server が使用するテスト用証明書がCA証明書として機能するように更新されています。これは、テスト環境でクライアントがサーバーの証明書を信頼できるようにするために必要でした。

前提知識の解説

1. TLS/SSL (Transport Layer Security / Secure Sockets Layer)

TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブサイトのHTTPS接続などで広く利用されており、通信の盗聴、改ざん、なりすましを防ぎます。TLSハンドシェイクと呼ばれるプロセスを通じて、クライアントとサーバー間で暗号化された通信チャネルを確立します。

2. SNI (Server Name Indication)

SNIは、TLSプロトコルの拡張機能の一つです。TLSハンドシェイクの初期段階で、クライアントが接続しようとしているサーバーのホスト名(ドメイン名)をサーバーに伝えます。

なぜSNIが必要か? かつては、一つのIPアドレスとポート番号には一つのTLS証明書しか関連付けられませんでした。しかし、仮想ホスティングの普及により、一つのIPアドレスで複数のドメイン(例: example.comanothersite.org)をホストするサーバーが増えました。これらのドメインがそれぞれ異なるTLS証明書を持つ場合、クライアントがIPアドレスとポート番号だけで接続すると、サーバーはどの証明書を提示すべきか判断できません。

SNIは、この問題を解決します。クライアントがホスト名を伝えることで、サーバーは適切な証明書を選択し、TLSハンドシェイクを正常に続行できます。SNIがない場合、サーバーはデフォルトの証明書を提示するか、ハンドシェイクに失敗する可能性があります。

3. Go言語の net/http パッケージ

Go言語の net/http パッケージは、HTTPクライアントとサーバーの実装を提供します。

  • http.Client: HTTPリクエストを送信し、HTTPレスポンスを受信するクライアントを表します。通常、http.DefaultClient を使用するか、カスタムの http.Client インスタンスを作成します。
  • http.Transport: http.Client が実際にネットワーク接続を確立し、リクエストを送信し、レスポンスを受信する際の低レベルな詳細を処理します。TLS設定(証明書、キー、検証設定など)は、http.TransportTLSClientConfig フィールドを通じて行われます。

4. Go言語の crypto/tls パッケージ

Go言語の crypto/tls パッケージは、TLSプロトコルの実装を提供します。

  • tls.Config: TLS接続の設定を保持する構造体です。この構造体には、証明書、秘密鍵、ルートCA証明書プール、そして ServerName などのフィールドが含まれます。
  • tls.Config.ServerName: クライアントが接続しようとしているサーバーのホスト名を指定します。この値は、SNI拡張としてサーバーに送信され、サーバーが適切な証明書を選択するために使用されます。また、サーバーから受け取った証明書の検証時にも、この ServerName と証明書のSubject Alternative Name (SAN) またはCommon Name (CN) が比較されます。

5. Certificate Authority (CA) 証明書

CA (Certificate Authority) は、デジタル証明書を発行し、その正当性を保証する信頼された第三者機関です。CA証明書は、他の証明書(サーバー証明書など)の署名を検証するために使用される公開鍵を含んでいます。クライアントは、信頼できるCAの証明書(ルートCA証明書や中間CA証明書)を事前に持っており、それらを使ってサーバーから提示された証明書が本物であることを検証します。

このコミットでは、httptest.Server が使用するテスト用証明書がCA証明書として変更されました。これは、テストクライアントがこのテストサーバーの証明書を信頼し、TLSハンドシェイクを正常に完了できるようにするためです。

6. httptest パッケージ

Go言語の net/http/httptest パッケージは、HTTPサーバーとクライアントのテストを容易にするためのユーティリティを提供します。httptest.NewTLSServer は、TLSを有効にしたテスト用HTTPサーバーを簡単に起動できる関数です。

技術的詳細

このコミットの技術的な核心は、net/http/transport.go 内の Transport 型の getConn メソッドにおける変更です。

  1. ServerName の自動設定: getConn メソッドは、HTTPリクエストの接続を確立する際に呼び出されます。HTTPS接続の場合、TLSハンドシェイクを開始する前に tls.Client 関数を呼び出します。この変更では、tls.Client に渡す tls.Config オブジェクトの ServerName フィールドが、リクエスト先のホスト名に基づいて自動的に設定されるようになりました。 具体的には、以下のロジックが追加されました。

    • 既存の t.TLSClientConfignil であるか、または ServerName が空文字列である場合、接続先のアドレス (cm.addr()) からホスト名 (host) を抽出します。
    • もし t.TLSClientConfignil であれば、新しい tls.Config インスタンスを作成し、その ServerName を抽出したホスト名に設定します。
    • もし t.TLSClientConfig が存在し、ServerName が空であれば、既存の tls.Config をシャローコピー(クローン)し、そのクローンに抽出したホスト名を ServerName として設定します。これにより、元の t.TLSClientConfig が変更されることなく、リクエスト固有の ServerName が適用されます。 この自動設定により、http.Client を利用する開発者は、SNIが必要な場合でも TLSClientConfig.ServerName を手動で設定する必要がなくなりました。
  2. httptest.Server 証明書のCA化: src/pkg/net/http/httptest/server.go 内の localhostCert (テストサーバーが使用する自己署名証明書) のASN.1構造が変更され、CA証明書として機能するように更新されました。

    • 具体的には、証明書の拡張フィールドに Basic Constraints: CA:TRUE が含まれるように変更されました。これにより、この証明書が他の証明書に署名できるCAとして扱われるようになります。
    • この変更は、client_test.go で追加された新しいテストケース (TestClientWithCorrectTLSServerName など) が、httptest.Server から提供される証明書を信頼して検証できるようにするために必要でした。テストクライアントは、このCA証明書をルートCAとして信頼することで、テストサーバーの証明書を検証できるようになります。
  3. 新しいテストケースの追加: src/pkg/net/http/client_test.go には、SNIの自動設定と検証を目的とした以下のテスト関数が追加されました。

    • newTLSTransport: httptest.Server のTLS設定からルートCA証明書を抽出し、それを含む http.Transport を作成するヘルパー関数。これにより、テストクライアントはテストサーバーの証明書を信頼できます。
    • TestClientWithCorrectTLSServerName: http.Client が正しい ServerName (この場合は 127.0.0.1) を設定してTLS接続を確立できることを検証します。サーバー側で受信したリクエストの r.TLS.ServerName をチェックしています。
    • TestClientWithIncorrectTLSServerName: http.Client が意図的に間違った ServerName を設定した場合に、TLSハンドシェイクが失敗し、適切なエラーが返されることを検証します。これは、ServerName の検証が正しく機能していることを確認するためです。

これらの変更により、Goの net/http クライアントは、SNIを透過的にサポートし、より堅牢なHTTPS通信を提供できるようになりました。

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

src/pkg/net/http/transport.go

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -379,7 +379,18 @@ func (t *Transport) getConn(cm *connectMethod) (*persistConn, error) {
 
  	if cm.targetScheme == "https" {
  		// Initiate TLS and check remote host name against certificate.
- 		conn = tls.Client(conn, t.TLSClientConfig)
+ 		cfg := t.TLSClientConfig
+ 		if cfg == nil || cfg.ServerName == "" {
+ 			host, _, _ := net.SplitHostPort(cm.addr())
+ 			if cfg == nil {
+ 				cfg = &tls.Config{ServerName: host}
+ 			} else {
+ 				clone := *cfg // shallow clone
+ 				clone.ServerName = host
+ 				cfg = &clone
+ 			}
+ 		}
+ 		conn = tls.Client(conn, cfg)
  		if err = conn.(*tls.Conn).Handshake(); err != nil {
  			return nil, err
  		}

src/pkg/net/http/httptest/server.go

--- a/src/pkg/net/http/httptest/server.go
+++ b/src/pkg/net/http/httptest/server.go
@@ -184,15 +184,15 @@ func (h *waitGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // "127.0.0.1" and "[::1]", expiring at the last second of 2049 (the end
  // of ASN.1 time).
 var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
-MIIBOTCB5qADAgECAgEAMAsGCSqGSIb3DQEBBTAAMB4XDTcwMDEwMTAwMDAwMFoX
+MIIBTTCB+qADAgECAgEAMAsGCSqGSIb3DQEBBTAAMB4XDTcwMDEwMTAwMDAwMFoX
 DTQ5MTIzMTIzNTk1OVowADBaMAsGCSqGSIb3DQEBAQNLADBIAkEAsuA5mAFMj6Q7
  qoBzcvKzIq4kzuT5epSp2AkcQfyBHm7K13Ws7u+0b5Vb9gqTf5cAiIKcrtrXVqkL
 -8i1UQF6AzwIDAQABo08wTTAOBgNVHQ8BAf8EBAMCACQwDQYDVR0OBAYEBAECAwQw
 -DwYDVR0jBAgwBoAEAQIDBDAbBgNVHREEFDASggkxMjcuMC4wLjGCBVs6OjFdMAsG
 -CSqGSIb3DQEBBQNBAJH30zjLWRztrWpOCgJL8RQWLaKzhK79pVhAx6q/3NrF16C7
 -+l1BRZstTwIGdoGId8BRpErK1TXkniFb95ZMynM=
 ------END CERTIFICATE-----
-`)\n+8i1UQF6AzwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCACQwEgYDVR0TAQH/BAgwBgEB
++/wIBATANBgNVHQ4EBgQEAQIDBDAPBgNVHSMECDAGgAQBAgMEMBsGA1UdEQQUMBKC
++CTEyNy4wLjAuMYIFWzo6MV0wCwYJKoZIhvcNAQEFA0EAj1Jsn/h2KHy7dgqutZNB
++nCGlNN+8vw263Bax9MklR85Ti6a0VWSvp/fDQZUADvmFTDkcXeA24pqmdUxeQDWw
++Pg==
++-----END CERTIFICATE-----`)\n 
  // localhostKey is the private key for localhostCert.
  var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----

コアとなるコードの解説

src/pkg/net/http/transport.go の変更

この変更は、TransportgetConn メソッド内、特にHTTPS接続を確立する部分にあります。

 		cfg := t.TLSClientConfig
 		if cfg == nil || cfg.ServerName == "" {
 			host, _, _ := net.SplitHostPort(cm.addr())
 			if cfg == nil {
 				cfg = &tls.Config{ServerName: host}
 			} else {
 				clone := *cfg // shallow clone
 				clone.ServerName = host
 				cfg = &clone
 			}
 		}
 		conn = tls.Client(conn, cfg)
  1. cfg := t.TLSClientConfig: まず、Transport に設定されている TLSClientConfigcfg 変数に代入します。
  2. if cfg == nil || cfg.ServerName == "": ここが重要な条件分岐です。
    • もし TLSClientConfig が全く設定されていない (nil) か、
    • または TLSClientConfig は設定されているものの、その中の ServerName フィールドが空文字列である場合、自動設定のロジックが実行されます。
  3. host, _, _ := net.SplitHostPort(cm.addr()): 接続先のアドレス (cm.addr()) からホスト名(例: example.com192.168.1.1)を抽出します。net.SplitHostPort は、host:port 形式の文字列をホストとポートに分割します。
  4. if cfg == nil { cfg = &tls.Config{ServerName: host} }:
    • もし元の TLSClientConfignil だった場合、新しい tls.Config インスタンスを作成し、抽出した hostServerName として設定します。
  5. else { clone := *cfg; clone.ServerName = host; cfg = &clone }:
    • もし元の TLSClientConfig が存在し、ServerName が空だった場合、既存の cfg をシャローコピー(*cfg で値渡し)して clone を作成します。
    • その cloneServerName を抽出した host に設定します。
    • そして、cfg 変数をこの clone を指すように更新します。このシャローコピーの目的は、Transport インスタンスに設定されている元の TLSClientConfig を変更することなく、この特定の接続のために一時的に ServerName を設定することです。これにより、同じ Transport を使用する他の接続に影響を与えることなく、SNIが適切に機能します。
  6. conn = tls.Client(conn, cfg): 最終的に、更新された(または新しく作成された)tls.Config を使用して tls.Client が呼び出され、TLSハンドシェイクが開始されます。この cfg には、適切な ServerName が設定されているため、SNIが自動的に機能します。

この変更により、Goの http.Client は、特別な設定なしにSNIをサポートするようになり、複数の仮想ホストを持つHTTPSサーバーへの接続がより簡単かつ堅牢になりました。

src/pkg/net/http/httptest/server.go の変更

localhostCert 変数に格納されている自己署名証明書のASN.1エンコーディングが変更されました。具体的には、証明書の Basic Constraints 拡張に CA:TRUE が含まれるように更新されています。

この変更は、新しいテストケースが httptest.Server の証明書を信頼して検証できるようにするために不可欠です。テストクライアントは、この localhostCert をルートCAとして認識し、それによって署名されたサーバー証明書を有効と判断できるようになります。これにより、テスト環境で実際のSNIの挙動を正確にシミュレートし、検証することが可能になります。

関連リンク

参考にした情報源リンク