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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるTLS証明書検証の挙動を修正するものです。具体的には、TLS接続のホスト名検証において、プロキシ情報ではなくTLSハンドシェイクで実際に使用されるホスト名を使用するように変更されています。

コミット

commit 1b6d4b5c0a33909c0f17328e6a45c53d939f5ace
Author: Christian Himpel <chressie@googlemail.com>
Date:   Tue Sep 25 09:22:13 2012 -0700

    net/http: use tls host instead of proxy, when checking against a certificate
    
    Fixes #4152.
    
    R=bradfitz
    CC=golang-dev
    https://golang.org/cl/6570045

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

https://github.com/golang/go/commit/1b6d4b5c0a33909c0f17328e6a45c53d939f5ace

元コミット内容

net/http: use tls host instead of proxy, when checking against a certificate

Fixes #4152.

R=bradfitz
CC=golang-dev
https://golang.org/cl/6570045

変更の背景

この変更は、Go言語のIssue #4152「cmd/go: go get behind proxy does not work」を修正するために行われました。このIssueは、go get コマンドがプロキシ経由でパッケージを取得しようとした際に失敗するという問題でした。

具体的には、TLS (Transport Layer Security) を使用したHTTPS接続において、サーバーから提示される証明書のホスト名検証が正しく行われていないことが原因でした。従来のコードでは、プロキシ経由の接続の場合に、プロキシのアドレスをホスト名として使用して証明書を検証しようとしていました。しかし、TLS証明書は最終的な接続先サーバーのホスト名に対して発行されるため、プロキシのアドレスで検証を行うと、証明書とホスト名が一致せず、検証エラーが発生していました。

この修正は、TLS接続のホスト名検証において、プロキシ情報ではなく、TLSハンドシェイクで実際に使用されるホスト名(つまり、最終的な接続先サーバーのホスト名)を使用するように変更することで、この問題を解決しています。

前提知識の解説

TLS (Transport Layer Security)

TLSは、インターネット上でデータを安全にやり取りするための暗号化プロトコルです。HTTPS (HTTP Secure) は、HTTP通信をTLSで暗号化したものです。TLSは、以下の主要な機能を提供します。

  • 暗号化: 通信内容を暗号化し、盗聴を防ぎます。
  • 認証: サーバーの身元を証明し、なりすましを防ぎます。クライアント認証も可能です。
  • 完全性: データが改ざんされていないことを保証します。

TLSハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために行う一連のプロセスです。このプロセスの中で、サーバーは自身の証明書をクライアントに提示し、クライアントはその証明書を検証します。

TLS証明書とホスト名検証

TLS証明書は、公開鍵と、その公開鍵が特定のエンティティ(ウェブサイト、サーバーなど)に属していることを証明するデジタル署名を含んでいます。証明書には、その証明書が有効なホスト名(Common NameやSubject Alternative Name)が記載されています。

クライアントがTLS接続を確立する際、サーバーから受け取った証明書に記載されているホスト名と、クライアントが接続しようとしているホスト名(URLのドメイン名など)が一致するかどうかを検証します。この検証が失敗すると、クライアントは接続を拒否し、セキュリティ警告を表示します。これは、中間者攻撃(Man-in-the-Middle attack)を防ぐために非常に重要です。

HTTPプロキシ

HTTPプロキシは、クライアントとサーバーの間に位置し、クライアントからのHTTPリクエストをサーバーに転送し、サーバーからのレスポンスをクライアントに転送するサーバーです。プロキシは、セキュリティ、キャッシュ、アクセス制御などの目的で使用されます。

プロキシ経由でHTTPS接続を行う場合、クライアントはまずプロキシに対して CONNECT メソッドを使用して、目的のサーバーへのTCPトンネルの確立を要求します。プロキシがトンネルを確立すると、クライアントはそのトンネルを介して目的のサーバーと直接TLSハンドシェイクを行います。この際、プロキシはTLS通信の内容には関与しません。

net.SplitHostPort

Go言語の net パッケージにある SplitHostPort 関数は、"host:port" 形式の文字列をホストとポートに分割するユーティリティ関数です。例えば、"example.com:443""example.com""443" に分割します。

技術的詳細

このコミットの核心は、net/http パッケージ内の Transport 型の getConn メソッドにおけるTLSホスト名検証ロジックの変更です。

getConn メソッドは、HTTPリクエストを送信するための接続を取得する役割を担っています。TLS接続の場合、このメソッド内でTLSハンドシェイクが開始され、サーバー証明書の検証が行われます。

変更前のコードでは、TLS証明書のホスト名検証を行う際に、cm.addr() の結果を net.SplitHostPort で分割してホスト名を取得していました。cm.addr() は、接続先のアドレスを返すメソッドですが、プロキシ経由の接続の場合、このアドレスがプロキシのアドレスを返す可能性がありました。

// 変更前
host, _, _ := net.SplitHostPort(cm.addr())

これにより、プロキシ経由のHTTPS接続において、TLS証明書の検証がプロキシのホスト名に対して行われてしまい、実際の目的サーバーのホスト名と一致しないため、検証エラーが発生していました。

変更後のコードでは、cm.addr() の代わりに cm.tlsHost() を呼び出すように修正されました。

// 変更後
host := cm.tlsHost()

cm.tlsHost() メソッドは、TLSハンドシェイクで実際に使用されるホスト名(つまり、最終的な接続先サーバーのホスト名)を返すように設計されています。これにより、TLS証明書のホスト名検証が正しいホスト名に対して行われるようになり、プロキシ経由のHTTPS接続でも証明書検証が成功するようになりました。

この修正は、Transport 型の TLSClientConfig フィールドが nil であるか、ServerName が空文字列である場合に適用されます。これは、明示的に ServerName が設定されていない場合に、自動的に正しいホスト名を推測して検証を行うためのロジックです。

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

src/pkg/net/http/transport.go ファイルの以下の行が変更されました。

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -381,7 +381,7 @@ func (t *Transport) getConn(cm *connectMethod) (*persistConn, error) {
 		// Initiate TLS and check remote host name against certificate.
 		cfg := t.TLSClientConfig
 		if cfg == nil || cfg.ServerName == "" {
-			host, _, _ := net.SplitHostPort(cm.addr())
+			host := cm.tlsHost()
 			if cfg == nil {
 				cfg = &tls.Config{ServerName: host}
 			} else {

コアとなるコードの解説

変更されたコードは、Transport 型の getConn メソッド内にあります。このメソッドは、HTTPリクエストを送信するための接続を確立する際に呼び出されます。

  1. cfg := t.TLSClientConfig: Transport オブジェクトに設定されているTLSクライアント設定を取得します。
  2. if cfg == nil || cfg.ServerName == "": TLS設定が全くないか、ServerName フィールドが明示的に設定されていない場合に、自動的にホスト名を決定するロジックに入ります。
  3. - host, _, _ := net.SplitHostPort(cm.addr()): 変更前のコードです。cm.addr() は接続先のアドレスを返しますが、プロキシ経由の場合、プロキシのアドレスを返す可能性がありました。それを net.SplitHostPort でホストとポートに分割し、ホスト名を取得していました。
  4. + host := cm.tlsHost(): 変更後のコードです。cm.tlsHost() は、TLSハンドシェイクで実際に使用されるホスト名(つまり、最終的な接続先サーバーのホスト名)を返します。これにより、正しいホスト名で証明書検証が行われるようになります。
  5. if cfg == nil { cfg = &tls.Config{ServerName: host} } else { cfg.ServerName = host }: 取得したホスト名を使用して、TLS設定の ServerName フィールドを設定します。これにより、TLSハンドシェイク時にこのホスト名がSNI (Server Name Indication) として送信され、サーバー証明書の検証にも使用されます。

この修正により、net/http クライアントがプロキシ経由でHTTPS接続を行う際に、TLS証明書のホスト名検証が正しく行われるようになり、go get コマンドなどのプロキシ利用時の問題が解決されました。

関連リンク

参考にした情報源リンク

  • Go Issue #4152のWeb検索結果
  • Go言語の net/http パッケージのドキュメント (当時のバージョン)
  • TLSプロトコルに関する一般的な情報
  • HTTPプロキシに関する一般的な情報